newhaneul

[Multimedia] PyOpenGL 본문

4. University Study/Multimedia

[Multimedia] PyOpenGL

뉴하늘 2026. 4. 18. 20:38
728x90

 

본 포스팅은 인하대학교 안남혁 교수님의 [202601-EEC4410-001] Multimedia을 수강하고 공부한 내용을 정리하기 위한 포스팅입니다.

 

1. GLSL Component Access

 

GLSL(OpenGL Shading Language)에서 벡터(Vector)와 행렬(Matrix) 내부의 데이터에 접근하고 조작하는 문법이다. 

 

벡터 요소 접근 (Vector Component Access)

GLSL에서는 배열처럼 인덱스 기호인 []를 사용하거나, 점(.) 연산자 뒤에 특정 문자를 붙여 벡터의 개별 값에 접근할 수 있다.

  • 인덱스는 C언어나 파이썬처럼 0부터 시작한다.
  • 점(.) 연산자를 사용할 때는 용도에 따라 세 가지 이름 묶음을 사용할 수 있다.
    • 위치 좌표계: x, y, z, w
    • 색상 좌표계: r, g, b, a
    • 텍스처 좌표계: s, t, p, q
      이들은 묶음의 이름만 다를 뿐 내부적으로는 완전히 동일한 순서의 메모리를 가리킨다.
      즉, 예시의 vec4 v에서 4번째 위치에 있는 값 4.4를 가져오려면 v[3], v.w, v.a, v.q를 모두 자유롭게 사용할 수 있다.

 

스위즐링 (Swizzling)

 스위즐링은 점(.) 연산자 뒤에 여러 개의 문자를 원하는 순서대로 조합하여, 한 번에 새로운 벡터를 만들어내는 매우 강력한 문법이다.

  • 조합 및 축소: v.xyz라고 쓰면 원본 벡터 v의 1, 2, 3번째 요소를 묶어 새로운 vec3 자료형을 만들어낸다.
  • 순서 변경: v.bgr처럼 문자의 순서를 섞으면, 값의 순서도 뒤집힌 상태(3.3, 2.2, 1.1)로 새로운 vec3가 생성된다.
  • 반복: v.tt처럼 같은 문자를 여러 번 쓰면, 동일한 값(여기서는 2.2)을 중복해서 가진 vec2를 생성할 수도 있다.

 

예상 문제: GLSL 벡터 및 행렬 접근 문법 (코드 빈칸 또는 오류 찾기)

 

[문제] 다음 GLSL 코드 스니펫에서 주석에 적힌 결과값을 얻기 위해 [빈칸]에 들어갈 올바른 코드를 작성하라. (각 2점)

 

vec4 v = vec4(1.0, 2.0, 3.0, 4.0);
mat3 m = mat3(
    1.0, 2.0, 3.0,
    4.0, 5.0, 6.0,
    7.0, 8.0, 9.0
);

// 1. v에서 x, y, z 요소만 추출하여 vec3 만들기
vec3 a = v.[빈칸 1]; // 기대값: vec3(1.0, 2.0, 3.0)

// 2. v의 맨 마지막 요소(w) 값을 float 변수로 추출
float b = v.[빈칸 2]; // 기대값: 4.0

// 3. 행렬 m의 두 번째 열(column) 전체를 vec3로 가져오기
vec3 c = m[빈칸 3]; // 기대값: vec3(4.0, 5.0, 6.0)

 

[정답 및 해설]

  • [빈칸 1]: xyz 또는 rgb 또는 stp (Swizzling을 이용해 필요한 성분만 추출 ).
  • [빈칸 2]: w 또는 a 또는 q (벡터의 4번째 요소에 접근 ).
  • [빈칸 3]: [1] (GLSL에서 행렬은 열 벡터(Column vector)의 배열로 취급되며, 인덱스는 0부터 시작하므로 두 번째 열은 인덱스 1이다 ).

 

2. VAO와 VBO를 초기화하는 코드

def initiallize_triangle(vertices):
    # create and activate VAO (vertexa array object)
    VAO = glGenVertexArrays(1)
    glBindVertexArray(VAO)

    # create and activate VBO (vertex buffer object)
    VBO = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, VBO)

    # copy vertex data to VBO
    glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)

    # configure vertex attribute (attribute 0: position)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * vertices.itemsize, None)
    glEnableVertexAttribArray(0)

    # configure color attribute (attribute 1: color)
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * vertices.itemsize, ctypes.c_void_p(3 * vertices.itemsize))
    glEnableVertexAttribArray(1)

    return VAO, VBO
  • glVertexAttribPointer(index, size, type, normalized, stride, pointer)
    • 위치(Position) 속성 설정 (Attribute 0)
      • glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * vertices.itemsize, None)
        • index: 해당 속성의 인덱스 번호를 뜻한다.
        • size: 위치가 X, Y, Z 3개의 값으로 구성됨을 의미한다.
        • stride: 하나의 정점이 X, Y, Z, R, G, B 총 6개의 실수로 이루어져 있으므로, 다음 정점의 X 위치로 넘어가려면 6칸을 건너뛰어야 한다는 뜻이다. (24 bytes)
        • pointer: 위치 데이터는 배열의 맨 처음부터 시작하므로 None (0)이 들어간다.
    • 색상(Color) 속성 설정 (Attribute 1)
      • glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * vertices.itemsize, 12)
        • pointer: 색상 데이터는 배열의 맨 처음이 아니라 앞의 X, Y, Z 3칸 (12 byte)을 지나서부터 시작된다. 따라서 데이터 읽기를 시작할 위치를 12로 설정해준다.
  • glEnableVertexAttribArray(0),  glEnableVertexAttribArray(1),
    • 방금 구조를 정의한 0번 위치와 1번 속성을 실제 렌더링 과정에서 사용할 수 있도록 켜준다.
    • Vertex Shader 코드에서도 layout(location = 0)과 layout(location = 1)을 선언하여 이 데이터를 받아주도록 설정한다.

 

예상 문제: VAO와 VBO 메모리 구조 계산

 강의 노트에서는 정점 위치(Position)와 색상(Color)을 하나의 배열에 교차(Interleaved) 배치한 뒤, glVertexAttribPointer에서 Stride와 Offset을 계산하는 과정을 매우 비중 있게 다루고 있다 . 이를 확장하여 위치, 법선(Normal), 텍스처 좌표가 포함된 문제로 출제될 수 있다.

 

[문제] 아래와 같이 3개의 attribute를 가지는 vertex를 PyOpenGL로 렌더링하고자 한다. 모든 attribute는 4 byte(GL_FLOAT)라고 가정한다. 아래 코드에서 빈칸 (A) ~ (F)에 들어갈 값을 채우라. (단, 파이썬의 ctypes.c_void_p() 형식에 맞춰 바이트 단위 정수값을 적을 것.) (10점)

  • attribute 0: vertex 좌표 (x, y, z)
  • attribute 1: normal 벡터 (nx, ny, nz)
  • attribute 2: texture 좌표 (s, t)
# vertex 좌표 (attribute 0)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, __(A)__, None)
glEnableVertexAttribArray(0)

# normal 벡터 (attribute 1)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, __(B)__, ctypes.c_void_p(__(C)__))
glEnableVertexAttribArray(1)

# texture 좌표 (attribute 2)
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, __(D)__, ctypes.c_void_p(__(E)__))
glEnableVertexAttribArray(__(F)__)

 

  • 정답: (A) 32, (B) 32, (C) 12, (D) 32, (E) 24, (F) 2
  • 해설: 하나의 정점(Vertex)은 3(pos) + 3(normal) + 2(tex) = 총 8개의 float 값으로 구성된다. float는 4바이트이므로 다음 정점까지의 보폭(Stride)은 $8 \times 4 = 32$바이트가 된다 .
    • Position Offset: 0바이트부터 시작하므로 None.
    • Normal Offset: 앞의 좌표 3개($3 \times 4$바이트)를 건너뛰어야 하므로 12바이트.
    • Texture Offset: 앞의 좌표 3개와 법선 3개(총 6개, $6 \times 4$바이트)를 건너뛰어야 하므로 24바이트.

 

3. 렌더링 루프와 그리기 호출

 

 

  • glUseProgram(shader_program): 앞으로 그릴 도형에 어떤 셰이더(질감, 색상, 좌표 변환 규칙 등)를 덮어씌울지 결정하고 해당 셰이더 프로그램을 활성화한다.
  • glBindVertexArray(VAO): GPU 메모리에 올려둔 정점 데이터(VBO)를 어떤 구조로 읽어올지에 대한 설정이 담긴 객체(VAO)를 바인딩하여 활성화한다. 즉, "저번에 설정해 둔 방식대로 데이터를 가져와라"라고 지시하는 것이다.
  • glDrawArrays(mode, first, count): 실제로 GPU에게 렌더링을 지시하는 최종 명령(Draw Call)이다.
    • mode: 어떤 원시 도형(Primitive)으로 그릴지 지정한다. 예를 들어 GL_TRIANGLES는 정점을 3개씩 묶어 삼각형으로 렌더링한다.
    • first: 정점 배열에서 몇 번째 인덱스부터 읽기 시작할지 정한다.
    • count: 렌더링할 총 정점의 개수를 지정한다

 

4. Uniform 변수 설정

 

 Uniform 변수는 각 정점마다 값이 달라지는 Attribute(위치 좌표, 정점 고유의 색상 등)와 달리, 한 번의 Draw Call이 실행되는 동안 모든 정점과 픽셀에 동일하게 유지되는 전역 변수 같은 개념이다. 보통 물체 전체의 변환 행렬(이동, 회전)이나 전체 색상을 하나로 통일해서 넘길 때 사용한다.

  • 가장 중요한 규칙: Uniform 값을 파이썬 등 메인 코드에서 셰이더로 업데이트하여 넘겨주려면, 반드시 그 Uniform이 속해 있는 셰이더 프로그램을 glUseProgram을 호출해 먼저 활성화해 두어야 한다. 셰이더가 활성화되지 않은 상태에서 값을 밀어 넣으려고 하면 코드가 정상적으로 작동하지 않는다.
  • glUniform3f(u_color_loc, 0, 0, 1): 미리 찾아둔 Uniform 변수의 위치(u_color_loc)에 실제 값을 집어넣는 함수다. 여기서 함수 이름 끝의 3f는 3개의 실수(float) 데이터를 인자로 받겠다는 뜻이다. (예: R(0), G(0), B(0) 색상값 전달).

 

5. NumPy와 OpenGL의 메모리 구조 차이

  • Row-major (NumPy 방식): 행(가로줄)을 기준으로 순서대로 메모리에 저장한다. [a, b, c, d] .
  • Column-major (OpenGL 방식): 열(세로줄)을 기준으로 메모리에 저장한다. [a, c, b, d] .

 이러한 차이 때문에, 파이썬에서 만든 NumPy 행렬을 OpenGL 셰이더로 그냥 넘겨버리면 값의 순서가 뒤죽박죽 섞여서 전혀 엉뚱한 방향으로 물체가 변형되거나 화면에서 사라지는 문제가 발생한다.

 

 이 문제를 해결하는 가장 쉬운 방법이 코드에 사용된 glUniformMatrix4fv 함수의 세 번째 인자인 transpose 플래그를 GL_TRUE로 설정하는 것이다.

  • glUniformMatrix4fv(world_loc, 1, GL_TRUE, ...)
  • 파이썬의 Row-major 행렬을 수학적으로 전치(Transpose, 행과 열을 뒤바꿈)시켜 버리면, 메모리상의 데이터 순서가 OpenGL이 기대하는 Column-major 순서와 완벽하게 일치하게 된다 .
while not glfwWindowShouldClose(window):
    glClear(GL_COLOR_BUFFER_BIT)
    glUseProgram(shader_program)

    # animations
    t = glfwGetTime()

    th = np.radians(t * 90)
    rotation_matrix = np.array([
        [np.cos(th), -np.sin(th), 0.0, 0.0],
        [np.sin(th),  np.cos(th), 0.0, 0.0],
        [np.sin(th),  np.cos(th), 1.0, 0.0],
        [0.0,         0.0,        0.0, 1.0]
    ])

    translation_matrix = np.array([
        [1.0, 0.0, 0.0, np.sin(t)],
        [0.0, 1.0, 0.0, 0.0],
        [0.0, 0.0, 1.0, 0.0],
        [0.0, 0.0, 0.0, 1.0]
    ])

    # note that 'transpose' (3rd parameter) is set to GL_TRUE
    # because numpy array is row-major
    glUniformMatrix4fv(world_loc, 1, GL_TRUE, translation_matrix)
    glBindVertexArray(object1_vao)
    glDrawArrays(GL_TRIANGLES, 0, 3)

    glUniformMatrix4fv(world_loc, 1, GL_TRUE, rotation_matrix)
    glBindVertexArray(object2_vao)
    glDrawArrays(GL_TRIANGLES, 0, 3)

 

예상 문제: NumPy와 OpenGL의 행렬 레이아웃 차이 

 강의 노트 후반부에서 가장 강조하는 파이썬 고유의 문제점은, NumPy는 행 우선(Row-major) 메모리 방식을 사용하고 OpenGL은 열 우선(Column-major) 방식을 사용한다는 점이다. 이를 Vertex Shader의 오류 찾기 문제로 변형하기 좋다.

 

[문제] 다음은 파이썬 코드에서 회전 변환 행렬을 생성하여 GLSL Vertex Shader의 worldMat uniform 변수로 전달하는 과정이다. 코드가 정상적으로 동작하지 않아 화면에 물체가 찌그러지거나 보이지 않는다. 잘못된 부분을 찾아 수정하고 그 이유를 설명하라. (5점)

# 파이썬 렌더링 루프 내부
rotation_mat = np.array([
    [np.cos(th), -np.sin(th), 0., 0.],
    [np.sin(th),  np.cos(th), 0., 0.],
    [0.,          0.,         1., 0.],
    [0.,          0.,         0., 1.]
], dtype=np.float32)

loc_world_mat = glGetUniformLocation(shader_program, "worldMat")
glUniformMatrix4fv(loc_world_mat, 1, GL_FALSE, rotation_mat)

 

[정답 및 해설]

  • 오류 부분: glUniformMatrix4fv(loc_world_mat, 1, GL_FALSE, rotation_mat) 호출 부분에서 GL_FALSE를 사용한 것이 잘못되었다.
  • 수정: GL_FALSE를 GL_TRUE로 변경해야 한다.
  • 이유: NumPy는 행 단위로 메모리가 연속되는 Row-major 방식을 사용하지만, OpenGL은 열 단위로 읽어들이는 Column-major 방식을 사용한다 . 따라서 NumPy 배열을 그대로 전달할 때는 OpenGL이 이를 올바른 행렬로 해석할 수 있도록 transpose 파라미터를 GL_TRUE로 설정하여 행과 열을 전치(Transpose)시켜 주어야 한다 .

 

2025-1 중간고사: Phong Shading (Vetex Shader)

 아래 코드는 Phong Shading을 구현할 때 사용되는 Vertex Shader 코드의 일부이다. 이 코드에서 잘못된 부분을 모두 찾아, 각 항목에 대해 무엇이 잘못되었는지와 그 이유를 설명하라.

layout (location = 0) in vec3 vin_pos;
layout (location = 1) in vec2 vin_st;
layout (location = 2) in vec3 vin_normal;

[ ... 생략 ... ]

void main() {
    vec4 world_pos = worldMat * vec4(vin_pos.xyz, 1.0);
    gl_Position = projMat * viewMat * world_pos;

    // world transform normal
    vec3 normal = mat3(worldMat) * vin_normal;

    vec3 l = normalize(light_pos - vec3(world_pos)); // light vectors
    vec3 v = normalize(eye - vec3(world_pos)); // view vector
    vec3 r = reflect(-l, normal);

    // specular term
    vec3 specular = pow(max(dot(r, v), 0.0), sh) * s_s * m_s;
    [ ... 생략 ... ]

    vout_color = vec4(diffuse + specular + ambient + emissive, 1.0);
}
  • vec3 normal = normalize(mat3(transpose(inverse(worldMat))) * vin_normal);
    • 비균등 스케일링(Non-uniform scaling)이 포함되어 있는 월드 변환 행렬(worldMat)을 법선 벡터에 변환을 적용할 때는 단순히 월드 행렬을 곱하는 것이 아니라 월드 행렬의 역행렬의 전치행렬, 즉 $(M^{-1})^T$ (Normal Matrix)를 곱해야 한다.

 

6. Tangent Space Normal Mapping

Tangent Space -> World Space (Vertex Shader)

void main()
{
    vec4 world_pos = worldMat * vec4(vin_pos, 1.0);

    // note: harde-coded T (1, 0, 0)
    vec3 T = normalize(mat3(worldMat) * vec3(1.0, 0.0, 0.0));
    vec3 N = normalize(mat3(inverse(transpose(worldMat))) * vin_normal);
    vec3 B = cross(N, T);
    vout_TBN = mat3(T, B, N);

    vout_st = vin_st;
    vout_pos = world_pos.xyz;

    gl_Position = projMat * viewMat * world_pos;
}
  • vec3 T = normalize(mat3(worldMat) * vec3(1.0, 0.0, 0.0));
  • vec3 N = normalize(mat3(inverse(transpose(worldMat))) * vin_normal);
  • vec3 B = cross(N, T);
  • vout_TBN = mat3(T, B, N);

 

Tangent Space -> World Space (Fragment Shader)

void main()
{
    // texturing
    vec3 color = texture(textureMap, vout_st).rgb;
    
    // normal map sampling
    vec3 sampled_normal = texture(normalMap, vout_st).rgb;
    sampled_normal = normalize(sampled_normal * 2.0 - 1.0);
    
    // normals: tangent -> world
    vec3 perturbed_normal = normalize(vout_TBN * sampled_normal);

    vec3 light_pos = vec3(0, 1, 5); // harde-coded light position
    vec3 eye = vec3(0, 0, 5); // harde-coded eye position

    // light and material properties
    float sh = 64;
    vec3 s_s = vec3(1.0, 1.0, 1.0);
    vec3 m_s = vec3(0.95, 0.95, 0.95);
    
    vec3 l = normalize(light_pos - vout_pos);

    vec3 v = normalize(eye - vout_pos);  // view vector
    vec3 r = reflect(-l, perturbed_normal);   // reflection vector
    float spec = pow(max(dot(r, v), 0.0), sh);
    vec3 specular = spec * s_s * m_s;
    
    float diff = max(dot(perturbed_normal, l), 0.0);
    vec3 diffuse = diff * color;
    fout_color = vec4(diffuse + specular, 1.0);
}
  • vec3 perturbed_normal = normalize(vout_TBN * sample_normal);

 

Tangent Space -> World Space (Vertex Shader)

void main()
{
    vec4 world_pos = worldMat * vec4(vin_pos, 1.0);

    // note: harde-coded T (1, 0, 0)
    vec3 T = normalize(mat3(worldMat) * vec3(1.0, 0.0, 0.0));
    vec3 N = normalize(mat3(inverse(transpose(worldMat))) * vin_normal);
    vec3 B = cross(N, T);
    vout_TBN = transpose(mat3(T, B, N));

    vout_st = vin_st;
    vout_pos = world_pos.xyz;

    gl_Position = projMat * viewMat * world_pos;
}
  • vec3 T = normalize(mat3(worldMat) * vec3(1.0, 0.0, 0.0));
  • vec3 N = normalize(mat3(inverse(transpose(worldMat))) * vin_normal);
  • vec3 B = cross(N, T);
  • vout_TBN = transpose(mat3(T, B, N));

 

Tangent Space -> World Space (Fragment Shader)

void main()
{
    // texturing
    vec3 color = texture(textureMap, vout_st).rgb;
    
    // normal map sampling
    vec3 sampled_normal = texture(normalMap, vout_st).rgb;
    sampled_normal = normalize(sampled_normal * 2.0 - 1.0);
    

    vec3 light_pos = vec3(0, 1, 5); // harde-coded light position
    vec3 eye = vec3(0, 0, 5); // harde-coded eye position

    // light and material properties
    float sh = 64;
    vec3 s_s = vec3(1.0, 1.0, 1.0);
    vec3 m_s = vec3(0.95, 0.95, 0.95);
    
    vec3 l = normalize(light_pos - vout_pos);
    vec3 v = normalize(eye - vout_pos);  // view vector

    // light, view vectors: world -> tangent space
    l = vout_TBN * l;
    v = vout_TBN * v;

    vec3 r = reflect(-l, sampled_normal);   // reflection vector
    float spec = pow(max(dot(r, v), 0.0), sh);
    vec3 specular = spec * s_s * m_s;
    
    float diff = max(dot(sampled_normal, l), 0.0);
    vec3 diffuse = diff * color;
    fout_color = vec4(diffuse + specular, 1.0);
}
  • l = vout_TBN * l;
  • v = vout_TBN * v;

 

[문제] 코드 빈칸: TBN 행렬 생성 및 변환 (Vertex Shader)

 다음은 빛의 방향 벡터와 시선 벡터를 탄젠트 공간으로 변환하기 위해 버텍스 셰이더에서 TBN 행렬을 구성하는 코드이다. 빈칸

( B )와 ( C )에 들어갈 올바른 GLSL 내장 함수를 작성하라.

vec3 T = normalize(mat3(worldMat) * vec3(1.0, 0.0, 0.0));
vec3 N = normalize(mat3(inverse(transpose(worldMat))) * vin_normal);

// 종법선(Bitangent) 벡터 계산
vec3 B = ( B )(N, T);

// 월드 -> 탄젠트 공간 변환을 위한 TBN 행렬의 역행렬(전치행렬) 구하기
vout_TBN = ( C )(mat3(T, B, N));

 

[정답]

  • ( B ) : cross
  • ( C ) : transpose

 

[문제] Tangent Space -> World Space 변환 (프래그먼트 셰이더)

[상황 설명]

조명 연산을 월드 공간(World Space) 기준으로 수행하려고 한다. 버텍스 셰이더에서는 $3 \times 3$ 크기의 vout_TBN 행렬을 구성하여 프래그먼트 셰이더로 넘겨주었다. 노멀 맵에서 읽어온 법선 벡터는 탄젠트 공간 기준이므로, 이를 월드 공간으로 변환해야 조명 벡터와 내적을 할 수 있다.

 

[문제]

다음 프래그먼트 셰이더 코드에서 빈칸 ( A )에 들어갈 변수명을 작성하라.

// 1. 노멀 맵에서 탄젠트 공간 기준의 법선 벡터 읽어오기 (-1.0 ~ 1.0으로 Unpacking)
vec3 tangent_normal = texture(normalMap, vout_st).rgb;
tangent_normal = normalize(tangent_normal * 2.0 - 1.0);

// 2. 탄젠트 공간의 법선 벡터를 월드 공간으로 변환
vec3 world_normal = normalize( ( A ) * tangent_normal );

// 3. 월드 공간 기준의 조명 연산 수행
float diff = max(dot(world_normal, lightDir), 0.0);

 

[정답 및 해설]

  • 정답: vout_TBN (또는 TBN)
  • 해설: T, B, N 벡터를 열(Column)로 쌓아 만든 TBN 행렬은 '탄젠트 공간의 벡터를 월드 공간으로 변환'하는 역할을 한다. 따라서 텍스처에서 추출한 tangent_normal에 TBN 행렬을 그대로 곱해주면 월드 공간의 법선 벡터를 얻을 수 있다.

 

[문제]

 다음 버텍스 셰이더 코드에서 빈칸 ( B )와 ( C )에 들어갈 알맞은 코드를 작성하라.

// 월드 공간 기준의 T, B, N 벡터 생성
vec3 T = normalize(mat3(worldMat) * vec3(1.0, 0.0, 0.0));
vec3 N = normalize(mat3(inverse(transpose(worldMat))) * vin_normal);
vec3 B = cross(N, T);

// TBN 행렬 구성 (Tangent -> World)
mat3 TBN = mat3(T, B, N);

// 역변환 행렬 구성 (World -> Tangent)
mat3 invTBN = ( B )(TBN);

// 조명 위치와 뷰 위치를 탄젠트 공간으로 변환하여 프래그먼트 셰이더로 전달
vout_tangent_light_pos = invTBN * light_pos;
vout_tangent_view_pos  = ( C ) * view_pos;

 

[정답 및 해설]

  • 정답: ( B ) transpose, ( C ) invTBN
  • 해설: TBN 행렬은 서로 직교하는 크기가 1인 벡터들로 이루어진 직교 행렬(Orthogonal Matrix)이다. 직교 행렬의 역행렬(Inverse)은 전치 행렬(Transpose)과 완벽히 동일하다는 수학적 특성이 있다. 따라서 무거운 inverse() 연산 대신 transpose() 연산을 사용하여 빠르고 가볍게 역변환 행렬(월드 $\rightarrow$ 탄젠트)을 구할 수 있다.

 

[문제] 조명 연산의 기준 공간 일치 (프래그먼트 셰이더)

[상황 설명] 문제 2번의 최적화를 적용하여 조명 위치(tangent_light_pos)와 시선 위치(tangent_view_pos)를 탄젠트 공간으로 변환하여 프래그먼트 셰이더로 넘겨받았다.

 

[문제] 이 상황에서 프래그먼트 셰이더 내부의 Phong 조명 연산을 수행할 때, 텍스처에서 읽어온 sampled_normal 벡터에 추가적인 행렬 곱셈 연산이 필요한지 판단하고 빈칸 ( D )에 들어갈 수식을 완성하라.

// 탄젠트 공간 기준의 픽셀 위치
vec3 tangent_frag_pos = vout_TBN * vout_pos; // (vout_TBN은 이미 transpose된 상태라고 가정)

// 빛 방향과 시선 방향 계산 (모두 탄젠트 공간 기준)
vec3 lightDir = normalize(tangent_light_pos - tangent_frag_pos);
vec3 viewDir  = normalize(tangent_view_pos - tangent_frag_pos);

// 노멀 맵에서 법선 벡터 Unpacking
vec3 sampled_normal = texture(normalMap, vout_st).rgb;
sampled_normal = normalize(sampled_normal * 2.0 - 1.0);

// Diffuse 연산을 위한 내적 수행
float diff = max(dot( ( D ), lightDir), 0.0);

 

[정답 및 해설]

  • 정답: ( D ) sampled_normal
  • 해설: 빛의 방향(lightDir)과 노멀 맵에서 읽어온 법선 방향(sampled_normal)이 모두 탄젠트 공간(Tangent Space)이라는 동일한 좌표계 안에 존재하게 되었다. 따라서 법선 벡터에 TBN 행렬을 곱하는 등의 추가 변환 없이, 두 벡터를 그대로 내적(dot)하여 조명 연산을 수행하면 된다. 이것이 버텍스 셰이더에서 미리 공간을 변환해 두는 TBN 최적화의 가장 큰 장점이다.
728x90