셰이더 프로그래밍- Part 3: 노말 매핑 (Normal mapping)

2023. 1. 5. 00:06·OpenGL/CS-248 셰이더 프로그래밍

Part 3: Normal mapping

Part2에서 만든 이미지입니다.

바닥평면과 구 물체에는 표면에 텍스쳐 맵을 추가해 디테일을 넣었음에도 굉장히 flat 해보입니다.

Part2 결과
Part2 결과

 

Part3에서는 normal mapping을 구현하여 표면이 디테일을 가진 것 처럼 보이도록 할 것입니다.

normal mapping의 아이디어는 'normal map'이라는 텍스쳐맵에 벡터 값을 저장해놓고 해당 벡터값을 표면의 normal vector로 사용하는 것입니다. Normal Vector를 표면의 점마다 정의한다면, 빛의 반사를 풍부하게 표면해 표면 질감이 더 도드라집니다.

우측 그림이 normal map의 예시입니다.

[fig1] 출처 : https://github.com/stanford-cs248/shading
[fig1] 출처 : https://github.com/stanford-cs248/shading

normal map의 RGB 값은 각각 (x,y,z) 벡터를 나타내고 이 벡터는 물체의 표면에서의 normal vector 방향을 의미합니다. 하지만 이 vector는 object space나 world space에서 정의된 vector가 아니라 표면의 tangent space에서 표현됩니다.

표면의 tangent space란, 각각의 표면점 마다 정의되는 space인데 각 점에서 normal vector를 Z축(그림에서 파랑)으로 기준삼고, 표면을 향하는 Tangent vector를 X축(그림에서 빨강)으로 정의하는 공간입니다.

이 Tangent vector는 모델 파일을 로드할 때 정의되어 있다면 vertex position, uv와 같이 불러올 수 있습니다.

 

normal mapping은 물체 표면의 점을 받아서, 이 점에 있는 적절한 위치의 vector (tangent space)를 normal map으로부터 가져옵니다. 이 tangent space vector를 world space vector로 변환하여 기존의 Reflection Model에서 사용되던 normal vector ($\hat{N}$)로 대체됩니다.

 

수정할 파일은

  • src/shader/shader.vert
  • src/shader/shader.frag
  • src/dynamic_scene/mesh.cpp:Mesh::internalDraw()

입니다.

 

 

shader.vert 파일입니다.

여기서는 tan2world , 즉 tangent space vector를 world space로 옮겨야합니다.

 

uniform mat4 obj2world;                 // object to world transform
uniform mat3 obj2worldNorm;             // object to world transform for normals
uniform vec3 camera_position;           // world space camera position           

uniform mat4 mvp;                       // model-view-projection matrix

uniform bool useNormalMapping;         // true if normal mapping should be used

// per vertex input attributes 
in vec3 vtx_position;            // object space position
in vec3 vtx_tangent;
in vec3 vtx_normal;              // object space normal
in vec2 vtx_texcoord;
in vec3 vtx_diffuse_color; 

// per vertex outputs 
out vec3 position;                  // world space position
out vec3 vertex_diffuse_color;
out vec2 texcoord;
out vec3 dir2camera;                // world space vector from surface point to camera
out vec3 normal;
out mat3 tan2world;                 // tangent space rotation matrix multiplied by obj2WorldNorm

void main(void)
{
    position = vec3(obj2world * vec4(vtx_position, 1));

    // TODO CS248 Part3: Normal Mapping
    // compute 3x3 tangent space to world space matrix here: tan2world
    
       
    // Tips:
    //
    // (1) Make sure you normalize all columns of the matrix so that it is a rotation matrix.
    //
    // (2) You can initialize a 3x3 matrix using 3 vectors as shown below:
    // vec3 a, b, c;
    // mat3 mymatrix = mat3(a, b, c)
    // (3) obj2worldNorm is a 3x3 matrix transforming object space normals to world space normals
    // compute tangent space to world space matrix
    
    normal = obj2worldNorm * vtx_normal;

    vertex_diffuse_color = vtx_diffuse_color;
    texcoord = vtx_texcoord;
    dir2camera = camera_position - position;
    gl_Position = mvp * vec4(vtx_position, 1);
}

input attributes를 보시면, vtx_tangent 값이 input으로 들어온 것을 볼 수 있습니다.

이 값은 [fig1] 그림에서 T vector와 같습니다.

[fig1] 그림에서의 N vector는 object space의 normal인 vtx_normal을 obj2worldNorm 매트릭스를 사용하여 world space로 보냈으니 normal 변수와 같겠습니다.

 

그렇다면 B vector만 계산하면 tangent coordinate의 NTB axis를 완성할 수 있겠습니다.

B vector는 N, T를 외적하면 구할 수 있습니다. (오른손 법칙에 주의하세요.)

tan2world라는 output 값을 계산해줍니다. 이 tan2world 값은 vector shader의 output으로 내보내지면 Part2에서 사용되는 퐁 쉐이딩 fragment shader코드의 input으로 넘겨집니다.

 

$\begin{equation*}
tan2world = 
\begin{pmatrix}
T_{x} & T_{y} & T_{z} \\
B_{x} & B_{y} & B_{z} \\
N_{x} & N_{y} & N_{z} 
\end{pmatrix}
\end{equation*}$

 

 

src/shader/shader.vert 하단의 코드는 다음과 같습니다.

	...
    gl_Position = mvp * vec4(vtx_position, 1);
    
    if (useNormalMapping) {
        vec3 B = normalize(cross(normal, vtx_tangent));
        vec3 T = normalize(vtx_tangent);

        // mat3 constructor is column-major
        tan2world = mat3(T, B, noraml);
    }

 

다음은 src/dynamic_scene/mesh.cpp:Mesh::internalDraw() 함수를 수정해봅시다.

여기는 간단한데요, 기존에 diffuseTexture를 사용하는 코드에서는 "diffuseTextureSampler"라는 이름으로 실제 텍스트 이미지를 셰이더쪽으로 넘겨줍니다.

        if (doTextureMapping_)
        	shader_->setTextureSampler("diffuseTextureSampler", diffuseTextureId_);

        // TODO CS248 Part 3: Normal Mapping:
        // You want to pass the normal texture into the shader program.
        // See diffuseTextureSampler for an example of passing textures.

 

위의 diffuseTexture코드를 참고하여 normalTexture도 셰이더로 넘겨줍니다.

        if (doTextureMapping_)
        	shader_->setTextureSampler("diffuseTextureSampler", diffuseTextureId_);

        // TODO CS248 Part 3: Normal Mapping:
        // You want to pass the normal texture into the shader program.
        // See diffuseTextureSampler for an example of passing textures.
        
        if (doNormalMapping_)
            shader_->setTextureSampler("normalTextureSampler", normalTextureId_);

 

 

 

마지막으로 src/shader/shader.frag 코드를 수정해봅시다.

현재는 normal값으로 world normal값을 보내주고 있습니다.

if (useNormalMapping) {
       // TODO: CS248 Part 3: Normal Mapping:
       // use tan2World in the normal map to compute the
       // world space normal baaed on the normal map.

       // Note that values from the texture should be scaled by 2 and biased
       // by negative -1 to covert positive values from the texture fetch, which
       // lie in the range (0-1), to the range (-1,1).
       //
       // In other words:   tangent_space_normal = texture_value * 2.0 - 1.0;

       // replace this line with your implementation
       N = normalize(normal);

    } else {
       N = normalize(normal);
    }

useNormalMapping이 true인 경우, normal 값을 normalTexture로부터 가져와 tangent space => world space로 변환해주면 됩니다.

주의할 점은 texture 특성상 rgb값이 0~1 값으로 채워져있는데요, 먼저 이 값을 -1~1 사이의 값으로 변환해주어야합니다.

 

먼저 , mesh.cpp:Mesh::internalDraw() 에서 넘겨주었던 "normalTextureSampler"를 sampler2D 타입으로 선언해줍니다.

//
// texture maps (line 12)
//

uniform sampler2D diffuseTextureSampler;

// TODO CS248 Part 3: Normal Mapping

uniform sampler2D normalTextureSampler;

 

그리고 다시 line 147로 가서,

normalTextureSampler 을 가져와 -1~1 사이의 값으로 변환해주고

vertex shader에서 넘겨받은 tan2world 매트릭스를 사용, tangent space normal 벡터를 world 벡터로 변환해줍니다.

마지막으로 normalize해주면 완성입니다.

if (useNormalMapping) {
       // TODO: CS248 Part 3: Normal Mapping:
       // use tan2World in the normal map to compute the
       // world space normal baaed on the normal map.

       // Note that values from the texture should be scaled by 2 and biased
       // by negative -1 to covert positive values from the texture fetch, which
       // lie in the range (0-1), to the range (-1,1).
       //
       // In other words:   tangent_space_normal = texture_value * 2.0 - 1.0;

       // replace this line with your implementation

        vec3 normal_rgb = texture(normalTextureSampler, texcoord).rgb;
        vec3 tangent_space_normal = normal_rgb * 2.0 - 1.0;

        N = tan2world * tangent_space_normal;
        N = normalize(N);

    } else {
       N = normalize(normal);
    }

 

컴파일 하고 실행해봅시다.

$ make
$ ./render ../media/spheres/spheres.json

 

 

Part3 결과
Part3 결과

 

가운데 벽돌 sphere의 표면이 좀 더 까칠하게 표현되고, 벽돌의 사이 부분의 빛 반사 효과가 그럴듯하게 보입니다.

 

 

Part3 결과(2)
Part3 결과(2)

 

바닥의 표면 역시 normalMapping이 적용되자 입체감이 생겼습니다.

 

이처럼 Normal Mapping을 사용한다면, 물체의 표면의 normal vector를 임의로 조작해 Phong-반사모델을 통해 입체감을 형성할 수 있습니다. Computer Graphics 분야에서 Low-polygon 메시에 normal map을 추가해서 값싼 비용으로 디테일을 추가할 수 있기 때문에 실무에서도 아주 유용하게 사용되고 있는 기술입니다.

 

하단의 이미지는 많은 polygon으로 표면을 표현한 경우 (좌), 1개의 polygon과 normalmap으로 표면을 표현한 경우입니다.

최적화 측면에서 우측이 훨씬 좋다고 볼 수 있습니다.

출처 : https://help.graphisoft.com/AC/18/INT/AC18Help/Appendix_Settings/Appendix_Settings-54.htm
출처 : https://help.graphisoft.com/AC/18/INT/AC18Help/Appendix_Settings/Appendix_Settings-54.htm

 

소스코드

https://github.com/Jooh34/shading

 

GitHub - Jooh34/shading: Stanford CS248 Assignment 3: Real-time Shading

Stanford CS248 Assignment 3: Real-time Shading. Contribute to Jooh34/shading development by creating an account on GitHub.

github.com

출처

https://help.graphisoft.com/AC/18/INT/AC18Help/Appendix_Settings/Appendix_Settings-54.htm

https://github.com/stanford-cs248/shading

'OpenGL > CS-248 셰이더 프로그래밍' 카테고리의 다른 글

Part 5-2 : 쉐도우 매핑 (Shadow Mapping) - 1  (0) 2023.01.17
셰이더 프로그래밍 - Part 5-1 : Spotlights 추가하기  (0) 2023.01.16
셰이더 프로그래밍 - Part 4: 환경광 추가하기  (0) 2023.01.15
셰이더 프로그래밍 - Part 2: 퐁 반사모델 구현하기  (0) 2023.01.03
셰이더 프로그래밍 - Part 1: Coordinate 변환 (Coordinate transform)  (2) 2023.01.01
'OpenGL/CS-248 셰이더 프로그래밍' 카테고리의 다른 글
  • 셰이더 프로그래밍 - Part 5-1 : Spotlights 추가하기
  • 셰이더 프로그래밍 - Part 4: 환경광 추가하기
  • 셰이더 프로그래밍 - Part 2: 퐁 반사모델 구현하기
  • 셰이더 프로그래밍 - Part 1: Coordinate 변환 (Coordinate transform)
jooh3444
jooh3444
게임엔진 / 그래픽스 개발 블로그
  • jooh3444
    Jooh 개발 블로그
    jooh3444
  • 전체
    오늘
    어제
    • Dev blog (18)
      • OpenGL (7)
        • CS-248 셰이더 프로그래밍 (7)
      • 언리얼 엔진 (7)
      • 기타 (1)
      • Computer Graphics (3)
  • 블로그 메뉴

    • 홈
    • About
    • github
  • 인기 글

  • 태그

    Shadow map
    Enviroment Lighting
    셰이더 프로그래밍
    Unreal Engine 5
    OpenGL
    셰이더
    twopass occlusion culling
    Blueprint load by path
    Unreal Engine
    그래픽스
    UE5 bugfix
    Computer Graphics
    범프 매핑
    Shader Programming
    Nanite
    Virtual Shadow Map
    bDontLoadBlueprintOutsideEditor
    multi-scattering brdf
    UE5
    Shader
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
jooh3444
셰이더 프로그래밍- Part 3: 노말 매핑 (Normal mapping)
상단으로

티스토리툴바