Advanced OpenGL(7) - Advanced GLSL

흑빡·2026년 7월 6일

그래픽스

목록 보기
42/50
post-thumbnail

쉐이더는 파이프라인화 되어있음
따라서 그 방식에 맞춰 uniform, vertex attributes, sampler등을 이용해 쉐이더 외부의 값을 이용해 쉐이더 파이프라인 내부에서 사용하도록 하는법을 공부했었음

GLSL내부에서 데이터를 입출력할 수 있는 몇가지 메서드가 있음
gl_로 시작하는 메서드인데 몇가지를 알아보도록 하자

gl_PointSize

glDrawArrays(GL_TRIANGLES, 0, 36);

이렇게 렌더링 옵션을 설정해줄 수 있었음

여기서 사용할 수 있는게
GL_POINTS

그리고 gl_PointSize = gl_position.z
이런식으로 사용해서 pointSize를 결정지어주면 됨

cpu, opengl에서 glPointSize를 해도 되지만
gpu에서 gl_PointSize하는게 더 낫겠지?

전체 사용은 다음과 같음

//...
glEnable(GL_GL_PROGRAM_POINT_SIZE);
//rendering
//...
glDrawArrays(GL_POINTS, 0, 36);
//...

//---------------
//vertex shader
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;

out vec2 TexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    TexCoords = aTexCoords;
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    gl_PointSize = gl_Position.z;
}

이렇게 멀어질수록 커지고, 가까워질수록 작아지는 큐브형태 점 완성~~

gl_VertexID

cpu에서 VBO를 통해 gpu로 데이터를 보내지 않고
gpu에서 바로 vertex id지정해서 사용할 수 있음

glDrawArrays(GL_TRIANGLES, 0, 6);

여기서 6개의 정점을 사용한다고 했음
그럼 gpu에서는 vertexId가 0~5까지 증가하는 id를 제공함

만약 glDrawElements를 사용한다면 index에 해당되는 번호를 제공함

따라서 vertex shader에서

#version 330 core
layout (location = 1) in vec2 aTexCoords;

out vec2 TexCoords;

void main()
{
    vec2 pos;
    if (gl_VertexID == 0) pos = vec2(-0.7,  1.0);
    else if (gl_VertexID == 1) pos = vec2(-1.0, -1.0);
    else if (gl_VertexID == 2) pos = vec2(1.0,  1.0);
    else if (gl_VertexID == 3) pos = vec2(-1.0, -1.0);
    else if (gl_VertexID == 4) pos = vec2(1.0, -1.0);
    else pos = vec2(1.0, 1.0);

    gl_Position = vec4(pos, 0.0, 1.0);
    TexCoords = pos * 0.5 + 0.5;
}

이렇게 vertexID를 이용해 삼각형을 그릴 수 있음

굳~~

gl_FragCoord

gl_FragCoord는 읽기 전용 변수임

fragment픽셀의 화면공간좌표 값임
vec4자료형으로 x,y,z,1w\frac{1}{w}값을 가짐

x,y는 화면공간의 x,y를 의미함
그냥 화면 가로/세로에서의 fragment좌표 인거임

z는 해당 fragment의 깊이를 의미함

1w\frac{1}{w}는 원근 투영시에 보간되는 값을 의미함
LearnOpenGL(6) - 좌표 시스템, 클립 공간의 원근 분할쪽을 보면 됨

1w\frac{1}{w}의 이유

w값은 원근투영시에 원근 분할시에
x,y,z와 각각 나누어 원근감을 주게 됨

하지만 나눗셈 연산은 연산비용이 좀 있음
따라서 각 fragment에 대해 w로 나누기보다
w의 역수인 1w\frac{1}{w}를 저장해두고 이를 곱하는 방식으로 연산비용을 줄일 수 있음

즉, 2D 화면공간에서 텍스처나 색상 데이터가 원근감에 맞게 왜곡 없이
보간되기 위해1w\frac{1}{w}가 쓰이는 거임

그럼 이걸 어떻게 사용하냐?

fragment shader를 수정하면 됨

#version 330 core
out vec4 FragColor;

void main()
{
    if (gl_FragCoord.x < 640 && gl_FragCoord.y < 360)
    {
        FragColor = vec4(0.0, 0.0, 1.0, 1.0);
    }
    else if (gl_FragCoord.x < 640 && gl_FragCoord.y > 360)
    { 
        FragColor = vec4(0.0, 1.0, 0.0, 1.0); 
    }
    else if (gl_FragCoord.x > 640 && gl_FragCoord.y < 360)
    {
        FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
    else
    {
        FragColor = vec4(1.0, 1.0, 0.0, 1.0);
    }
}

간단하게 fragment를 4사분면으로 나눠서
각각 다른 색상을 지정해준거임

굳~~~

gl_FrontFacing

그냥 fragment shader에서 읽기 전용인 변수임

해당 fragment가 앞면이면 true, 아니면 false를 반환함

따라서 보이는 면의 texture와 안보이는 면의 texture를 다르게 설정해줄 수 있음

하지만 Advanced OpenGL(4) - face culling
여기서 살펴본 face culling을 활성화한다면 gl_FrontFacing는 무용지물이 됨

gl_FragDepth

이번엔 읽기변수가 아닌 입력변수임

값을 수정할 수 있다는거

gl_FragDepth는 fragment의 깊이를 수정할 수 있는 변수임
이 변수를 사용하지 않을때는 자동으로 gl_FragCoord.z를 사용함

하지만 직접 사용하지 않는 편이 최적화 관점에서는 더 좋음

Advanced OpenGL(1) - Depth test, early depth test에서 알 수 있듯
조기 깊이 테스트를 할 수 없음

openGL 4.2이상에서는 가능

!!!!! 하지만 GL4.2버전 이상에서부턴 shader 위에 layout으로 설정을 통해 가능하게 할 수 있음

#version 420 core // 4.2 OPENGL
out vec4 FragColor;
layout (depth_<condition>) out float gl_FragDepth;

void main()
{             
    FragColor = vec4(1.0);
    gl_FragDepth = gl_FragCoord.z + 0.1;
} 

여기 layout의 depth_<condition>를 이용해 조기 깊이테스트를 가능하게 할 수 있음

condition은 다음과 같음

condition설명
any기본값, 조기 깊이 테스트가 비활성화
greatergl_FragCoord.z에 비해 깊이 값을 더 크게만 만들 수 있음
lessgl_FragCoord.z에 비해 깊이 값을 더 작게만 만들 수 있음
unchangedgl_FragCoord.z에 사용중인 값이 그대로 사용됨

unchanged는 z값에 변경이 없다는걸 의미함
any를 제외한 모든 condition은 early depth testing이 활성화된다는거 잊지말고~~


인터페이스 블록

앞의 모든 챕터에서 shader를 다룰때,
in, out이라는걸 이용해 각 shader사이에서 데이터를 사용하도록 했음

하지만 어플리케이션의 규모가 커지면
당연히 in, out해야할 변수도 많아지고
이걸 하나로 관리하고 싶어질때가 올거임

이때 사용하는게 구조체랑 비슷한 인터페이스 블록임

코드로 보는게 더 이해가 빠를거임

아래는 vertex shader코드

#version 330 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out VS_OUT
{
    vec2 TexCoords;
} vs_out; //이름 변경 가능

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);    
    vs_out.TexCoords = aTexCoords;
}

아래는 fragment shader코드

#version 330 core

out vec4 FragColor;

in VS_OUT //인터페이스 이름 통일
{
    vec2 TexCoords;
} fs_in; //원하는 이름으로 사용 가능

uniform sampler2D texture;

void main()
{             
    FragColor = texture(texture, fs_in.TexCoords);   
}

이렇게 볼 수있듯이

인터페이스 이름과 내부 변수의 이름만 통일하고(필수)
사용할 인터페이스 변수명은 다르게 가져갈 수 있음

Uniform buffer object

지금까지 모든 챕터에서 glsl의 uniform을 설정하려고 할때
각각 개별의 uniform으로 설정했음

예를들어 오브젝트를 화면 공간에 넣어서 출력하기위해 계산하는 여러 행렬이 있었음

model, view, projection행렬이 그것이었음 ㅇㅇ

이 각각의 행렬을 uniform을 통해

cubeShader.setMat4("model", model);
cubeShader.setMat4("view", view);
cubeShader.setMat4("projection", projection);

이렇게 각각 데이터를 넣어줬음

근데 ubo(Uniform buffer object)를 이용하면 이렇게 각각 넣어줄 필요 없이
uniform을 공통되는 역할로 묶어 하나의 uniform으로 관리할 수 있음

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

layout (std140) uniform Matrices
{
    mat4 projection;
    mat4 view;
};

uniform mat4 model;

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

사용예시로 Matrices라는 uniform블럭 안에 projection, view를 냅두고 model은 따뤄 함 냅둬봄

그럼에도 불구하고 제대로 동작한다는거임

자, 이제 여기서 1가지 모르겠는 부분이 코드에서 보일거임

천천히 살펴보자

Uniform block layout

유니폼 블럭 레이아웃이라 부르는게 있음

layout (std140)ㅇㅇ

해당 uniform block이 어떻게 gpu에 저장될지를 결정짓는 요소임

예를들어

layout (std140) uniform ExampleBlock
{
    float value;
    vec3  vector;
    mat4  matrix;
    float values[3];
    bool  boolean;
    int   integer;
};

이런 uniform block이 있다고 해보자

기본 layout은 layout (shared)
shared layout은 gpu가 자체적으로 최적화를 진행해서
cpu에서 우리가 구조체를 만들어 넘겨줘도 메모리 위치를 확실하게 모르기때문에

  1. glGetUniformBlockIndex를 이용해 shader프로그램의 uniform을 읽음
  2. glGetUniformIndices를 이용해 해당 uniform의 블럭 변수들을 다시 읽음
  3. glGetActiveUniformsiv를 이용해 각 블럭 변수들의 offset을 가져옴
  4. glBufferSubData등을 이용해 값 넣기

layout(packed)도 존재함

이는 offset을 최대한으로 압축해서 사용하는 layout임
컴파일러마다 내부 구조가 달라져서 사실상 공유되는 uniform일수 없는거임

float은 n, vec3는 4n, vec2는 2n, mat4는 4*4n, 배열은 4\xn, bool은 n, int는 n 이런식으로 offset이 결정되게 됨

layout은 우리가 원하는 offset으로 지정할 수 있지만,
혹은 하드웨어에서 직접 원하는 방식대로 offset을 지정하고 최적화 하도록 할 수도 있음

이 offset을 올바르게 지정하지 못하면 무슨 에러가 생길지 모름
따라서 되도록이면 우리가 직접 지정하고 사용하는게 좋음

그렇게 사용자가 직접 지정하는 엄격한 offset을 layout(std140)이라고 하는거임
OpenGL과 cpp의 primitive 타입은 결국 상호호환되니까 ㅇㅇ
그리고 업계 표준이 layout(std140)이고 ㅇㅇ

위 예시 코드의 offset을 계산하면 아래와 같음

layout (std140) uniform ExampleBlock
{
                     // 기본 정렬  // 정렬된 오프셋
    float value;     // 4               // 0 
    vec3 vector;     // 16              // 16  (오프셋은 16의 배수여야 하므로 4->16)
    mat4 matrix;     // 16              // 32  (열 0)
                     // 16              // 48  (열 1)
                     // 16              // 64  (열 2)
                     // 16              // 80  (열 3)
    float values[3]; // 16              // 96  (values[0])
                     // 16              // 112 (values[1])
                     // 16              // 128 (values[2])
    bool boolean;    // 4               // 144
    int integer;     // 4               // 148
};

uniform block, uniform buffer object사용

이 사진을 봐보자

여러 shader에서 binding points라는걸 통해 값을 받아올 수 있음
이때 binding points에 데이터는 ubo를 통해 넣어주게 됨

그럼 어떤 binding points에 어떤 데이터를 넣어줘야하는지
어떻게 결정할까?

정답은 우리가 직접 결정해주면됨

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

out vec2 TexCoords;

layout(std140) uniform UniformBlockView
{
    mat4 view;
};
layout(std140) uniform UniformBlockProjection
{
    mat4 projection;
};

uniform mat4 model;

void main()
{
    TexCoords = aTexCoords;
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}

이렇게 vertex shader를 수정해줌

일부러 여러개의 binding points를 설정하는걸 보여주기위해 예제를 짬

여러개의 shader에서 동일한 2개의 uniform block을 사용할때 사용한다는 가정하에 코드를 짜는거지비

uint32_t redUBView = glGetUniformBlockIndex(redCubeShader.ID, "UniformBlockView");
uint32_t redUBProjection = glGetUniformBlockIndex(redCubeShader.ID, "UniformBlockProjection");
uint32_t greenUBView = glGetUniformBlockIndex(greenCubeShader.ID, "UniformBlockView");
uint32_t greenUBProjection = glGetUniformBlockIndex(greenCubeShader.ID, "UniformBlockProjection");
glUniformBlockBinding(redCubeShader.ID, redUBView, 0);
glUniformBlockBinding(redCubeShader.ID, redUBProjection, 1);
glUniformBlockBinding(greenCubeShader.ID, greenUBView, 0);
glUniformBlockBinding(greenCubeShader.ID, greenUBProjection, 1);
    
uint32_t uboView, uboProjection;
glGenBuffers(1, &uboView);
glGenBuffers(1, &uboProjection);
    
glBindBuffer(GL_UNIFORM_BUFFER, uboView);
glBufferData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), nullptr, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// binding point에 들어갈 데이터의 크기 정의
//glBindBufferBase(GL_UNIFORM_BUFFER, 0, uboView);
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboView, 0, sizeof(glm::mat4));
    
glBindBuffer(GL_UNIFORM_BUFFER, uboProjection);
glBufferData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), nullptr, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// binding point에 들어갈 데이터의 크기 정의
//glBindBufferBase(GL_UNIFORM_BUFFER, 1, uboProjection);
glBindBufferRange(GL_UNIFORM_BUFFER, 1, uboProjection, 0, sizeof(glm::mat4));

이렇게 binding points를 공유하도록 하면 됨

원하는 형식, 크기, 값을 넘겨주면 됨

  1. glGetUniformBlockIndex를 이용해 uniform block의 index를 찾고,
  2. glUniformBlockBinding를 이용해 해당 uniform block index에 binding points 번호를 부여
  3. glBufferData로 사용할 버퍼의 크기 부여
  4. glBindBufferBase혹은glBindBufferRange로 binding points의 번호에 데이터 넣기

이제 렌더링에서 사용하기만 하면 되는데 엄청 간단함

while(...)
{
	//...
    
    glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        
    glm::mat4 model = glm::mat4(1.0f);
        
    glm::mat4 view = camera.GetViewMatrix();
    glBindBuffer(GL_UNIFORM_BUFFER, uboView);
    glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(view));
    glBindBuffer(GL_UNIFORM_BUFFER, 0);
        
    glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
    glBindBuffer(GL_UNIFORM_BUFFER, uboProjection);
    glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
    glBindBuffer(GL_UNIFORM_BUFFER, 0);
        
    // cubes
    glBindVertexArray(cubeVAO);
    redCubeShader.use();
    model = glm::mat4(1.0);
    model = glm::translate(model, glm::vec3(-0.7f, 0.0f, 0.0f));
    redCubeShader.setMat4("model", model);
    glDrawArrays(GL_TRIANGLES, 0, 36);
        
    greenCubeShader.use();
    model = glm::mat4(1.0);
    model = glm::translate(model, glm::vec3(0.7f, 0.0f, 0.0f));
    greenCubeShader.setMat4("model", model);
    glDrawArrays(GL_TRIANGLES, 0, 36);
}
  1. uniform block에 해당되는 uniform 값을 할당함
  2. draw를 함

그럼 이렇게 되어버리는거지비

uniform block사용 이유

  1. 여러 uniform을 개별적으로 데이터를 입력하는것보다, 하나의 uniform block에 할당하고 데이터를 입력하는게 더 빠름
  2. 여러 쉐이더에서 동시에 변경되어야 하는 uniform변수의 경우 단 한번의 수정으로 해결가능 -> 유지보수굳
  3. 일반 uniform보다 uniform block에 할당가능한 메모리 크기가 더 큼

3번이 무슨소리인가 하면...
skeletal animation이라는게 있음
단순하게 설명하면 3D모델을 리깅을 통해 애니메이팅한거임

리깅을 하려면 bone이라는 개념이 필요함
bone은 말그대로 뼈라는 뜻으로 관절을 의미하는데,
요구사항에 따라 다르겠지만 고퀄 3D모델의 애니메이션에는 많은 관절이 필요함

일반 uniform으로 해당 bone들의 shader program을 짜야하면 uniform의 크기는 제한적이기 때문에 쉐이더가 터질 가능성이 있음

이때 사용하는게 uniform block으로 일반 uniform보다 더 많은 메모리를 할당할 수 있기때문에, 이를 사용하는편이 정신건강에 더 좋은거임

profile
그래픽스 하는 퍼그

0개의 댓글