ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Horizon Mapping
    Graphics/Graphics Study 자료 2022. 3. 15. 22:46

    Horizon Mapping

    최초 작성 : 2022-03-15

    마지막 수정 : 2022-03-15

    최재호

     

    목차

    1. 목표  
    2. Horizon Mapping  
     2.1. 돌출된 부분에 의해서 생기는 그림자 계산
     2.2. Horizon Map 생성
     2.3. Weight Cube Map 생성
     2.4. Ambient Occlusion Map 생성
     2.5. Horizon Mapping 렌더링
    3. Ambient Occlusion 렌더링
    4. 실제 구현 코드
    5. 실제 구현 결과
    6. References

     

    1. 목표

    이전에 알아본 Parallax Mapping은 표면과 뷰벡터 사이의 각이 작아질수록 돌출된 부분이 미흡하게 표현되는 단점을 보완하는 방식이었습니다. Horizon Mapping은 여기서 한 발짝 더 나아가 돌출된 부분에 의해서 나타나는 그림자를 추가하여 지오메트리의 디테일을 더욱더 살려주는 기법입니다. FGED2에 나오는 Horizon Mapping에 대해서 알아보고 실제 구현을 이해해봅시다.

    사전 지식
    Normal Mapping
    Tangent Space
    Parallax Mapping


    2. Horizon Mapping

    2.1. 돌출된 부분에 의해서 생기는 그림자 계산

     

    그림1. HorizonMapping 핵심 알고리즘. 현재 텍셀의 높이인 h0과 라이트 방향에 있는 이웃 텍셀의 높이인 h1라 뒀을때, h0, h1로 생성한 벡터와 표면이 이루는 각을 a 로 두고, 표면과 라이트가 이루는 각을 b로 둠. 이 a와 b의 대소비교를 통해 현재 텍셀이 차폐되어있는지 판단함. a < b 이면 라이트가 이웃 텍셀보다 더 높게 위치하므로 차폐되지 않음. (출처: 레퍼런스1)

     

    Horizon Mapping 라이트로 부터 차폐를 판정하는 핵심 과정은 다음과 같습니다. (그림1 함께 참고)
    1). 현재 텍셀 위치 (x, y)의 Height H0와 현재 텍셀에서 라이트의 방향의 이웃에 있는 텍셀 (x + i, y + j)의 Height H1를 얻습니다. 이렇게 얻어진 두 Height가 생성하는 벡터와 표면이 이루는 각을 a를 구합니다.
    2). 라이트의 방향 벡터와 표면이 이루는 각을 b를 구합니다.
    3). 만약 “라이트 방향 벡터와 표면이 이루는 각 b” > “두 Height H0, H1가 생성하는 벡터와 표면이 이루는 각 a” 이면 현재 픽셀을 라이팅 합니다. 왜냐하면 라이트가 돌출된 부분보다 더 위에 있다는 의미이기 때문입니다. 만약 그렇지 않으면 현재 픽셀에는 그림자를 드리웁니다.

     

    핵심 아이디어를 보면 라이트의 방향벡터가 필요한 것을 알 수 있습니다. 라이트의 방향의 어디서든 들어올 수 있습니다. 그래서 현재 픽셀을 기준으로 주변 360도에 대한 모든 인접한 텍셀의 높이 차이(dh = h1-h0)를 알아야 합니다. (이 dh 를 사용하여 각 "두 Height H0, H1가 생성하는 벡터와 표면이 이루는 각 a"를 얻습니다.)

     

    주변 360도에 대한 모든 인접한 텍셀의 각도 정보는 Horizon Map에 저장됩니다. 이제 Horizon Map을 어떻게 구성하는지 알아봅시다.


    2.2. Horizon Map 생성

    먼저 HorizonMap에서 생성되는 어떤 식으로 데이터를 구성하는지 간단히 설명한 후 코드에서 작업하는 것을 정리해 보려고 합니다.

     

    HorizonMap은 360도의 인접 텍셀에 대한 높이 차를 저장하기 위해서 2장으로 구성됩니다. 2장의 텍스쳐는 총 8개의 채널이 있으며 각각은 0, 45, 90, 135, 180, 225, 270, 315 에 대한 높이차를 저장합니다. 그림 2를 보면 2가지 텍스쳐에 대해서 HorizonMap이 어떻게 구성되는지 볼 수 있습니다.

     

    각각의 8개의 방향은 해당 방향의 정보만 저장하는 것이 아니라 인접한 픽셀들의 평균 각도 a(dh로 부터 유도된)를 저장합니다. 실제로 HorizonMap에서 정보를 가져올 때, 현재 텍셀에서 22.5도 방향의 각도 a를 얻어오고 싶다면 0도와 45도의 a를 각각 가져와 보간하여 사용합니다. ( 0도의 a * 0.5 + 45도의 a * 0.5 = 실제 사용하는 a)

     

    HorizonMap에 저장되는 값은 라디안값이 아니라 sin(a) 값이 저장됩니다. 여기서의 a 는 그림1에서 나오는 두 Height가 생성하는 벡터와 표면이 이루는 각입니다. sin 값은 라디안 값이 커질수록 증가하기 때문에 이 값 자체로 대소 비교가 가능합니다. sin 값으로 저장하게 되면 얻을 수 있는 장점은 표면과 라이트가 이루는 각을 계산할 때(이 각을 b로 둠), 라이트가 정규화되어있다면, 라이트 벡터의 z 컴포넌트가 z / 1 = sin(b) 이기 때문에 바로 대소 비교가 가능하기 때문입니다. 

     

    또 하나 기억해둘 점은 여기서 사용되는 라이트 벡터는 탄젠트 공간에서의 라이트 벡터입니다.

     

    그림2. HorionMap은 2장(총 8채널)로 이루어져있으며 각 채널당 45도 단위로 저장하여 360 전방향에 있는 이웃텍셀과 현재 텍셀간의 높이차이 정보를 저장함. (출처: 레퍼런스1)

     

     

    아래 그림3의 HorizonMap의 코드 생성 부분에서 하는 작업을 설명한 내용입니다. 현재 텍셀을 기준으로 Horizon radius안에 있는 모든 텍셀에 대해서 현재 텍셀과의 고저차를 먼저 캐싱합니다. 그런 뒤 360도를 45도 단위로 총 8개의 부분으로 쪼갠다음 2장의 텍스쳐(총 8채널)에 각각 저장합니다. (4.1. HorizonMap의 생성 코드 의 구현 내용입니다.)

    그림3. HorizonMap 생성코드 설명 (출처: 직접 작성)

     

     

     

    2.3. Weight Cube Map 생성

     

    HorizonMap 2장 모두 생성했고, 라이트의 방향벡터를 알고 있다고 합시다. 그렇다면 8개의 HorizonMap 채널에서 라이트의 방향 벡터를 기준으로 어떤 채널을 사용할지 결정해야 합니다. 이 부분을 편하게 하기 위해서 이 책에서는 16x16x16 크기의 Weight를 담고 있는 CubeMap을 생성합니다. 그리고 여기서 만든 CubeMap에 라이트 벡터를 사용하여 샘플링하면 8개의 채널 중 어떤 채널들을 사용해야 할지 알 수 있을 것입니다.

     

    여기서 생각해볼 점이 CubeMap 또한 4개의 채널밖에 없습니다. 이렇게 되면 8개의 채널에 대한 Weight값을 1개의 CubeMap에서 얻을 수 없습니다. 그렇다고 CubeMap을 2개 사용하게 되면 이것 또한 부하가 될 것입니다. 이 책에서는 이 부분을 해결하기 위해서 0 ~ 1 사이 값을 HorizonMap 1번에 0 ~ -1 사이값을 HorizonMap 2번에 사용합니다. 이렇게 사용 가능한 이유는 HorizonMap1과 2가 저장하는 이웃 텍셀의 방향을 보면 서로 180도 정반대 방향입니다. 정반대 방향에 있는 경우 한쪽의 Weight가 1이 되면 반대쪽은 반드시 0이 됩니다. 그렇기 때문에 양수/음수로 나누어서 1개의 CubeMap에 총 8개의 Weight값을 저장할 수 있습니다.

     

    마지막으로 CubeMap을 사용하여 샘플링하는 경우 CubeMap의 각각의 6개의 면이 이어지는 쪽이 서로 연결되지 않아서 라이트가 회전하는 경우 그림자가 튀는 현상이 있을 수 있습니다. 이 부분은 OpenGL에서는 아래 명령을 사용하면 하드웨어에서 각각의 CubeMap 면들을 부드럽게 이어줘서 문제를 해결할 수 있습니다.

    glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);

     

    CubeMap의 생성과정은 HorizonMap의 구현과 이해해 크게 중요한 부분은 아니라고 판단하여 이 정도만 보고 넘어가도록 하겠습니다. 생성 코드는 4.2. Weight 정보를 담은 CubeMap 생성코드 에서 확인할 수 있습니다.

    그림4. 라이트 방향에 따른 Weight 정보가 담긴 CubeMap (출처: 레퍼런스1)

     

     

    2.4. Ambient Occlusion Map 생성

     

    Ambient Occlusion Map의 경우 이름과는 다르게 차폐된 정도가 아니라 라이팅의 양을 나타냅니다. 이 값을 Ambient Occlusion Map 의 싱글 채널에 저장합니다.

     

    이웃 텍셀과 높이차 때문에 차폐되어 들어오지 못하는 라이트를 고려해야 할 것입니다. 그렇다면 아래 그림처럼 Z축을 기준으로 차폐되지 않은 각도까지 (90-a) 들어온 라이트의 양을 적분해야 할 것입니다. 또한 이 값은 램버트 효과를 적용해야 할 것입니다. 아래 그림5를 봐주세요.

    그림5. AmbientLight를 받는 구간 θ (출처: 레퍼런스1)

     

    이 θ 구간에 들어오는 라이트를 램버트 효과를 적용하여 적분하게 되면 아래 그림6의 식이 나옵니다. 그리고 이 적분식을 풀어내면 cos(a) 가 나옵니다.

    그림6. AmbientLight를 받는 구간 θ에 대해 램버트 효과를 반영한 적분식, 이 적분식을 풀면 cos(a) 가 됨. (출처: 레퍼런스1)

     

    AmbientLight는 현재 텍셀 기준 360도 모든 이웃 텍셀 방향에서 들어오는 값을 고려합니다. 그래서 주변의 모든 이웃에 대한 coa(a)를 구한 후 평균값을 사용합니다. 실제 구현에는 AmbientPower 값을 추가로 두어 값을 조정 가능하게 합니다.

     

    2.5. Horizon Mapping 적용

     

    HorizonMap의 실제 사용은 아주 간단합니다. 그림7를 보면, 실제 사용해야 할 라이트 방향의 이웃과 현재 텍셀의 높이차를 나타내는 sin(a)을 바로 구해내는 식이 있습니다. 이후에 라이트의 z 컴포넌트와 대소 비교를 하면 현재 텍셀에 그림자를 드리울지 여부를 알 수 있습니다.

     

    그림7의 HorizonMap1과 2는 각각 h0, h1에 해당합니다. 그리고 w는 라이트 방향벡터를 사용하여 CubeMap에서 샘플링해온 Weight 값입니다. 이 값은 +의 경우 HorizonMap1, -의 경우 HorizonMap2에 Weight 값에 해당한다고 하였습니다. 이런 점을 사용하여 각각의 HorizonMap1, 2에 맞는 Weight값을 max 를 사용하여 얻어냅니다. 그리고 그 결과를 내적하게 되면 각각의 HorizonMap과 w의 Weighted Sum을 구할 수 있을 것입니다.

    그림7. CubeMap에서 얻어온 Weight 값과 HorizonMap의 Weighted Sum을 구해서 라이트 방향에 해당하는 이웃 텍셀과 현재 텍셀이 이루는 sin(a) 값을 얻는 식

     

    쉐도우가 드리우는 부분인지 확인하기 위해서 간단히 대소 연산을 하게 되면 쉐도우의 가장자리가 날카롭게 될 것입니다. 이 부분을 완화해주기 위해서 그림8과 같은 식을 사용합니다. 이 식에서 n 부분이 높을수록 쉐도우의 테두리가 더 날카롭게 사용됩니다. 그림9와 그림10를 참고해주세요.

    그림8. ShadowHardness를 조정하는 식

     

    그림9. ShadowHardness 차이 비교 (출처: 레퍼런스1)

     

    그림10. ShadowHardness 값의 변화에 따라서 값의 라이트 강도의 감쇄 기울기가 더 가파르게 되는 것을 확인할 수 있음 (출처: 직접 구현)

     

     

    3. Ambient Occlusion 렌더링

    AmbientOcclusion의 경우 즉시 텍스쳐에서 값을 샘플링해온 후 LightIntensity 에 곱해주면 됩니다.

     

     

    4. 실제 구현 코드

    4.1. HorizonMap의 생성 코드

    // HorizonMap의 생성
    void ConstructHorizonMap(Vector4* OutHorizonMap, const float* InHeightMap, float* OutAmbientMap
      , float InAmbientPower, int32 InWidth, int32 InHeight)
    {
      concurrency::parallel_for(size_t(0), size_t(InHeight), [&](size_t y)  // parallel for [0, InHeight-1]
      {
        constexpr int kAngleCount = 32;
        constexpr float kAngleIndex = float(kAngleCount) / (2 * PI);
        constexpr int kHorizonRadius = 16;
    
        Vector4* CurrentHorizonMap = OutHorizonMap + InWidth * y;
        float* CurrentAmbientMap = OutAmbientMap + InWidth * y;
        const float* pCenterRow = InHeightMap + y * InWidth;
    
        for(int32 x = 0; x < InWidth; ++x)
        {
          float h0 = pCenterRow[x];
          float maxTan2[kAngleCount] = {};
    
          for(int32 j = -kHorizonRadius + 1; j <kHorizonRadius; ++j)
          {
            const float* pRow = InHeightMap + ((y + j) & (InHeight - 1)) * InWidth;
            for(int32 i = -kHorizonRadius + 1; i < kHorizonRadius; ++i)
            {
              int32 r2 = i * i + j * j;
              if ((r2 < kHorizonRadius * kHorizonRadius) && (r2 != 0))
              {
                float dh = pRow[(x + i) & (InWidth - 1)] - h0;
                if (dh > 0.0f)
                {
                  float direction = atan2(float(j), float(i));
                  float delta = atan(0.7071f / sqrt(float(r2)));
                  int32 minIndex = int32(floor((direction - delta) * kAngleIndex));
                  int32 maxIndex = int32(ceil((direction + delta) * kAngleIndex));
    
                  float t = dh * dh / float(r2);
                  for(int32 n = minIndex; n <= maxIndex; ++n)
                  {
                    int32 m = n & (kAngleCount - 1);
                    maxTan2[m] = fmax(maxTan2[m], t);
                  }
                }
              }
            }
          }
    
          Vector4* pLayerData = CurrentHorizonMap;
          for(int32 layer = 0;layer<2;++layer)
          {
            Vector4 color(0.0f, 0.0f, 0.0f, 0.0f);
            int32 firstIndex = kAngleCount / 16 + layer * (kAngleCount / 2);
            int32 lastIndex = firstIndex + kAngleCount / 8;
    
            for(int32 index = firstIndex;index <= lastIndex;++index)
            {
              float tr = maxTan2[(index - kAngleCount / 8) & (kAngleCount - 1)];
              float tg = maxTan2[index];
              float tb = maxTan2[index + kAngleCount / 8];
              float ta = maxTan2[(index + kAngleCount / 4) & (kAngleCount - 1)];
    
              color.x += sqrt(tr / (tr + 1.0f));
              color.y += sqrt(tg / (tg + 1.0f));
              color.z += sqrt(tb / (tb + 1.0f));
              color.w += sqrt(ta / (ta + 1.0f));
            }
    
            pLayerData[x] = color / float(kAngleCount / 8 + 1);
            pLayerData += InWidth * InHeight;
          }
    
          float sum = 0.0f;
          for (int32 k = 0; k < kAngleCount; ++k)
            sum += 1.0f / sqrt(maxTan2[k] + 1.0f);
    
          // To adjust cos(a) in shader, AmbientMap saved radian.
          // CurrentAmbientMap[x] = pow(sum * (1.0f / float(kAngleCount)), InAmbientPower);
          CurrentAmbientMap[x] = pow(sum * (1.0f / float(kAngleCount)), InAmbientPower);
          CurrentAmbientMap[x] = acos(CurrentAmbientMap[x]);
        }
      });
    }

     

    4.2. Weight 정보를 담은 CubeMap 생성코드

    // Weight 정보를 담은 CubeMap 생성
    void GenerateHorizonCube(Vector4* texel)
    {
      for (int32 face = 0; face < 6; face++)
      {
        for (float y = -0.9375f; y < 1.0f; y += 0.125f)
        {
          for (float x = -0.9375f; x < 1.0f; x += 0.125f)
          {
            Vector2 v;
            float r = 1.0f / sqrt(1.0f + x * x + y * y);
            switch (face)
            {
            case 0: v = Vector2(r, -y * r); break;
            case 1: v = Vector2(-r, -y * r); break;
            case 2: v = Vector2(x * r, r); break;
            case 3: v = Vector2(x * r, -r); break;
            case 4: v = Vector2(x * r, -y * r); break;
            case 5: v = Vector2(-x * r, -y * r); break;
            }
            //v = Vector2(v.y, v.x);
            float t = atan2(v.y, v.x) / (PI / 4);
            float red = 0.0f;
            float green = 0.0f;
            float blue = 0.0f;
            float alpha = 0.0f;
            if (t < -3.0f) { red = t + 3.0f; green = -4.0f - t; }
            else if (t < -2.0f) { green = t + 2.0f; blue = -3.0f - t; }
            else if (t < -1.0f) { blue = t + 1.0f; alpha = -2.0f - t; }
            else if (t < 0.0f) { alpha = t; red = t + 1.0f; }
            else if (t < 1.0f) { red = 1.0f - t; green = t; }
            else if (t < 2.0f) { green = 2.0f - t; blue = t - 1.0f; }
            else if (t < 3.0f) { blue = 3.0f - t; alpha = t - 2.0f; }
            else { alpha = 4.0f - t; red = 3.0f - t; }
            *texel = Vector4(red, green, blue, alpha);
            ++texel;
          }
        }
      }
    }

     

    4.3. Horizon Mapping 버택스 쉐이더 코드

    // Horizon Mapping 버택스 쉐이더 코드
    #version 330 core
    
    precision mediump float;
    
    layout(location = 0) in vec3 Pos;
    layout(location = 1) in vec4 Color;
    layout(location = 2) in vec3 Normal;
    layout(location = 3) in vec3 Tangent;
    
    uniform mat4 M;
    uniform mat4 MVP;
    uniform vec3 EyeWorldPos;
    uniform vec3 LightDirection;    // from light to location
    
    out vec2 TexCoord_;
    out vec4 Color_;
    out mat3 TBN;
    out vec3 WorldSpaceViewDir;
    
    void main()
    {
        TexCoord_ = (Pos.xz + 0.5);
      Color_ = Color;
      gl_Position = MVP * vec4(Pos, 1.0);
      vec3 WorldPos = (M * vec4(Pos, 1.0)).xyz;
      WorldSpaceViewDir = normalize(EyeWorldPos - WorldPos);
    
      vec3 T = normalize(vec3(M * vec4(Tangent, 0.0)));
      vec3 B = normalize(vec3(M * vec4(cross(Normal, Tangent), 0.0)));
      vec3 N = normalize(vec3(M * vec4(Normal, 0.0)));
    
      TBN = mat3(T, B, N);
    }

    4.3. Horizon Mapping 픽셀 쉐이더 코드

    // Horizon Mapping 픽셀 쉐이더 코드
    #version 330 core
    
    precision mediump float;
    
    uniform sampler2D tex_object;    // diffuse
    uniform sampler2D tex_object2;    // normalmap
    uniform sampler2D tex_object3;    // height map
    uniform sampler2D tex_object4;    // horizon map layer1
    uniform sampler2D tex_object5;    // horizon map layer2
    uniform samplerCube tex_object6;  // Weight Cube map
    uniform sampler2D tex_object7;    // Ambient Occlusion map
    uniform int TextureSRGB[1];
    uniform int UseTexture;
    uniform int FlipedYNormalMap;
    
    uniform vec3 LightDirection;    // from light to location
    uniform vec2 TextureSize;
    uniform float HeightScale;      // s = (max height / 1 texel with), this was using when the normalmap was generated.
    uniform float NumOfSteps;      // Num of ParallaxMap iteration steps
    uniform float HorizonHeightScale;
    uniform float AmbientOcclusionScale;
    
    in vec2 TexCoord_;
    in vec4 Color_;
    in mat3 TBN;
    in vec3 WorldSpaceViewDir;
    
    out vec4 color;    // final color of fragment shader.
    
    // Fetching normal from normal map
    vec3 GetNormal(vec2 uv)
    {
      vec3 normal = texture(tex_object2, uv).xyz;
      normal = normal * 2.0 - 1.0;
      return normal;
    }
    
    // Shifting UV by using Parallax Mapping
    vec2 ApplyParallaxOffset(vec2 uv, vec3 vDir, vec2 scale)
    {
      vec2 pdir = vDir.xy * scale;
      
      if (FlipedYNormalMap > 0)
        pdir.y = -pdir.y;  // because opengl texture y is inverted
    
      for (int i = 0; i < NumOfSteps; ++i)
      {
        // This code can be replaced with fetching parallax map for parallax variable(h * nz)
        float nz = GetNormal(uv).z;
        float h = (texture(tex_object3, uv).x * 2 - 1);
        float parallax = nz * h;
        /////////////////////////////////
    
        uv += pdir * parallax;
      }
    
      return uv;
    }
    
    float ApplyHorizonMap(vec2 texcoord, vec3 ldir)
    {
      const float kShadowHardness = 2.0;
    
      // Read horizon channel factors from cube map.
      vec3 dir = vec3(ldir.x, -ldir.y, ldir.z);
      vec4 weights = texture(tex_object6, dir.xyz);
    
      // Extract positive and negative weights for horizon map layers 0 and 1.
      vec4 w0 = clamp(weights, vec4(0.0), vec4(1.0));
      vec4 w1 = clamp(-weights, vec4(0.0), vec4(1.0));
    
      // Sample the horizon map and multiply by the weights for each layer.
      float s0 = dot(texture(tex_object4, texcoord), w0);
      float s1 = dot(texture(tex_object5, texcoord), w1);
    
      float sum = clamp((s0 + s1)*HorizonHeightScale, 0, 3.14*2);
      return clamp(((ldir.z - sum) * kShadowHardness + 1.0), 0.0, 1.0);
    }
    
    float ApplyAmbientOcclusion(vec2 uv)
    {
      // To adjust cos(a) in shader, AmbientMap has radian.
      return cos(texture2D(tex_object7, uv).x * AmbientOcclusionScale);
    }
    
    void main()
    {
      vec2 uv = TexCoord_;
    
      mat3 transposeTBN = transpose(TBN);
      vec3 TangentSpaceViewDir = normalize(transposeTBN * WorldSpaceViewDir);
      
      // Ally Parallax Mapping : adjusting UV
      vec2 scale = HeightScale / (2.0 * NumOfSteps * TextureSize);
      uv = ApplyParallaxOffset(uv, TangentSpaceViewDir, scale);
      uv = clamp(uv, vec2(0.0), vec2(1.0));
    
      // NormalMap으로 부터 normal을 얻어오고, TBN 매트릭스로 변환시켜줌
      vec3 normal = GetNormal(uv);  
      normal = normalize(TBN * normal);
    
      // 라이팅 연산 수행
      float LightIntensity = 1.0f;
      LightIntensity = clamp(dot(normal, -LightDirection), 0.0, 1.0);
    
      // Apply HorizonMapping
      vec3 TangentSpaceLightDirFromSurface = normalize(transposeTBN * (-LightDirection));
      LightIntensity *= ApplyHorizonMap(uv, TangentSpaceLightDirFromSurface);
    
      // Fetching Diffuse texture
      if (UseTexture > 0)
      {
        color = texture2D(tex_object, uv);
        if (TextureSRGB[0] > 0)
          color.xyz = pow(color.xyz, vec3(2.2));
      }
      else
      {
        color = Color_;
      }
    
      LightIntensity *= ApplyAmbientOcclusion(uv);
      color.xyz *= vec3(LightIntensity);
    }

     

    5. 실제 구현 결과

     

    그림11. HorizonMapping 결과1 (AmbientOcclusion 미적용) (출처: 직접구현)

     

     

    그림12. HorizonMapping 결과2&amp;nbsp;(AmbientOcclusion 미적용)&amp;nbsp;(출처: 직접구현)

     

    그림13. HorizonMapping 결과3 (AmbientOcclusion 미적용)&amp;nbsp;(출처: 직접구현)

     

    그림14. Diffuse 텍스쳐에 AmbientOcclusion 적용한 결과&amp;nbsp;(출처: 직접구현)

     

    그림15. DiffuseOnly vs NormalMapping vs Parallax Mapping vs Horizon Mapping 비교 (출처 : 직접구현)

     

    5. 실제 구현 결과

    https://github.com/scahp/Shadows/tree/HorizonMapping

     

    6. References

    1. https://foundationsofgameenginedev.com/

     

     

     

     

     

    'Graphics > Graphics Study 자료' 카테고리의 다른 글

    Dual-depth relief interior mapping  (0) 2022.06.11
    Weighted Blended OIT  (0) 2022.05.23
    Parallax Mapping  (0) 2022.02.27
    Color Science  (2) 2021.12.25
    Atmospheric Shadowing  (0) 2021.04.14

    댓글

Designed by Tistory & scahp.