개발/OpenGL / / 2022. 3. 18. 22:08

OpenGL 공부일지 - OpenGL Super Bible 쉐이더와 프로그램 - 2

반응형

Compiling, Linking, and Examining Programs

opengl은 컴파일러와 링커를 가지고 이것들로 쉐이더 코드를 받아 내부 바이너리 형태로 컴파일한 후 링크시켜 그래픽스 프로세서 상에서 동작하도록 한다. 이 작업이 실패한다면 원인을 파악할 수 있어야한다. 또 컴파일이나 링크에 성공하더라도 프로그램이 이상하게 동작하는 원인을 알아야한다.

 

Getting Information from the Compiler

컴파일러가 쉐이더 코드의 문제를 발견하도록 하자.

void glGetShaderiv(GLuint shader,
    GLenum pname,
    GLint * params);

glCompileShader()를 호출하면 glGetShaderiv()를 통해 컴파일 결과를 확인할 수 있다.

glGetShaderiv의 pname인자의 사용할 수 있는 값들은 다음과 같다.

  • GL_COMPILE_STATUS : 쉐이더가 성공적으로 컴파일 되었는지 확인
  • GL_SHADER_TYPE : 쉐이더 객체의 타입 반환
  • GL_DELETE_STATUS : glDeleteShader가 쉐이더 객체에 대해 호출한지 여부에 따라 GL_TRUE, GL_FALSE 반환
  • GL_SHADER_SOURCE_LENGTH : 쉐이더 객체의 소스 코드 전체 길이 반환
  • GL_INFO_LOG_LENGTH : 쉐이더 객체에 포함된 로그 기록의 길이 반환
    정보 로그는 쉐이더의 컴파일 시 생성
void glGetShaderInfoLog(GLuint shader,
    GLsizei bufSize,
    GLsizei * length,
    GLchar * infoLog);

위 함수를 통해 쉐이터 객체에서 로그를 확인할 수 있다.

// Create, attach source to, and compile a shader...
GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, &source, NULL);
glCompileShader(fs);

// Now, get the info log length...
GLint log_length;
glGetShaderiv(fs, GL_INFO_LOG_LENGTH, &log_length);

// Allocate a string for it...
std::string str;

str.reserve(log_length);

// Get the log...
glGetShaderInfoLog(fs, log_length, NULL, str.c_str());

위와 같은 방식으로 glGetShaderiv를 이용해서 쉐이더로부터 컴파일러 로그를 얻는다.

 

#version 450 core

layout (location = 0) out vec4 color;

uniform scale;
uniform vec3 bias;

void main(void)
{
	color = vec4(1.0, 0.5, 0.2, 1.0) * scale + bias;
}

위 쉐이더 코드는 의도적으로에러를 포함시켰다.

ERROR: 0:5: error(#12) Unexpected qualifier
ERROR: 0:10: error(#143) Undeclared identifier: scale
WARNING: 0:10: warning(#402) Implicit truncation of vector from
size: 4 to size: 3
ERROR: 0:10: error(#162) Wrong operand types: no operation "+" exists
that takes a left-hand operand of type "4-component vector of vec4" and
a right operand of type "uniform 3-component vector of vec3" (or there
is no acceptable conversion)
ERROR: error(#273) 3 compilation errors. No code generated

그 결과로 이러한 에러를 띄웠다고 하자. 

첫 번째 에러는 5번째 줄 uniform scale을 지적하는데, 이는 유니폼의 타입을 빼먹은 것이다.

그 다음 세 개의 이슈인 10번째 줄의 에러문구들을 보자.

Undeclared identifier : scale : 컴파일러가 scale이 무엇인지 알지 못한다는 의미이다. 이전의 scale 타입 에러의 탓이다.

Implicit truncation of vector from size:4 to size:3 : 4요소 타입을 3요소로 변환시 잘림 현상 주의경고이다.

그 다음에러는 scale을 vec4 타입으로 지정해도 bias가 vec3이므로 연산할 수 없어 생기는 에러이다. 즉 타입을 vec4로 설정해주어야한다.

#version 450 core

layout (location = 0) out vec4 color;

uniform vec4 scale;
uniform vec4 bias;

void main(void)
{
	color = vec4(1.0, 0.5, 0.2, 1.0) * scale + bias;
}

위와 같이 수정하면 문제가 해결된다.

Getting Information from the Linker

만약 컴파일이 실패한다면 프로그램의 링크도 실패하거나 원하는대로 동작하지 않을 것이다.

glCompileShader() 호출 시 컴파일러가 로그를 생성한 것 처럼, glLinkProgram() 호출에도 링커는 궁금한 질의에 대한 로그를 생성한다. 

void glGetProgramiv(GLuint program,
    GLenum pname,
    GLint * params);

링크된 프로그램은 컴파일된 쉐이더보다 더 많은 상태를 가지는데, 위 함수를 사용해서 모두 얻을 수 있다.

 

위 glGetProgramiv()는 glGetShaderiv()와 매우 유사하다. pname의 값에 따라 정보를 얻을 수 있다.

  • GL_DELETE_STATUS : 쉐이더와 동일, 프로그램 객체에 대해 glDeleteProgram()의 호출 여부 반환
  • GL_LINK_STATUS : 쉐이더에서 GL_COMPILE_STATUS와 유사, 프로그램 링크의 성공 여부 반환
  • GL_INFO_LOG_LENGTH : 정보 로그 길이 반환
  • GL_ATTACHED_SHADERS : 프로그램에 attach된 쉐이더 개수 반환
  • GL_ACTIVE_ATTRIBUTES : 프로그램의 버텍스 쉐이더가 실제로 사용하는 속성의 개수 반환
  • GL_ACTIVE_UNIFORMS : 프로그램이 사용하는 유니폼 개수 반환
  • GL_ACTIVE_UNIFORM_BLOCKS : 프로그램이 사용하는 유니폼 블록 개수 반환

 

void glGetProgramInfoLog(GLuint program,
    GLsizei bufSize,
    GLsizei * length,
    GLchar * infoLog);

위 함수를 사용해서 프로그램의 정보 로그를 얻을 수 있다.

 

#version 450 core

layout (location = 0) out vec4 color;

vec3 myFunction();

void main(void)
{
	color = vec4(myFunction(), 1.0);
}

위 코드에는 myFunction이라는 외부함수가 있다. opengl은 myFunction의 본체가 프로그램 객체에 attach된 프래그먼트 쉐이더 중 하나에 정의가 있을것이라고 가정한다. (하나의 프로그램 객체에는 동일한 타입의 여러 쉐이더를 attach시켜 링크할 수 있다) 

glLinkProgram() 호출 시 opengGL은 모든 프래그먼트 쉐이더를 뒤져 myFunction을 찾고 없다면 에러를 출력한다. 위의 프래그먼트 쉐이더 코드만 객체에 링크하면 

Vertex shader(s) failed to link, fragment shader(s) failed to link.
ERROR: error(#401) Function: myFunction() is not implemented

이러한 에러가 나올 것이다. 그렇기 때문에 myFunction의 본체를 위 코드와 같은 쉐이더 코드 안에 포함하거나, 혹은 함수 본체를 포함하는 두 번째 프래그먼트 쉐이더를 동일한 프로그램 객체에 attach 시켜야한다.

Separate Programs

여태까지는 모든 프로그램을 단일 객체라고 생각하였다. 즉 활성화된 각 스테이지에 대해 하나의 쉐이더가 있다고 본 것이다. 각각 하나의 버텍스 쉐이더, 프래그먼트 쉐이더, 테셀레이션이나 지오메트리 쉐이더를 하나의 프로그램 객체에 attach 시키고 glLinkProgram()을 호출하여 프로그램 객체에 링크시켜 파이프라인을 구성하였다. 

 이러한 방식은 융통성이 떨어져 애플리케이션 성능에 지장을 줄 수 있다. 따라서 버텍스, 프래그먼트 및 쉐이더들의 모든 조합에 대해 고유한 프로그램 객체를 만들어야 한다.

 

만약 프래그먼트 쉐이더만 변경한다고 해보자. 단일 프로그램 구조상 동일한 버텍스 쉐이더를 둘 이상의 다른 프래그먼트 쉐이더에 링크할 필요가 생긴다. 단일 프로그램은 이런 상황에서 새로운 프로그램 객체를 만들어서 조합을 처리해야하는데, 여러 프래그먼트 쉐이더 및 여러 버텍스 쉐이더가 있다면, 각 조합에 대한 프로그램 객체가 필요하다. 

 따라서 쉐이더를 추가 할수록 쉐이더 스테이지를 혼합 할수록 복잡해진다. 

 

 위와 같은 문제 때문에 opengl에서는 프로그램 객체 분리 모드 링크를 지원한다. opengl 파이프라인의 각 섹션에 해당하는 여러 프로그램 객체가 하나의 프로그램 파이프라인 객체에 attach되고, 실시간으로 합쳐진다. 하나의 프로그램 객체에 attach된 쉐이더들은 스테이지간 최적화의 이점을 보는 동시에, 프로그램 파이프라인 객체에 어태치된 프로그램 객체도 상대적으로 적은 성능 부하만으로 변경할 수 있다.

 

glProgramParameteri()를 통해 프로그램 객체를 분리모드로 사용할 수 있다.

그 다음 glGenProgramPipeLines()를 통해 프로그램 파이프라인 객체를 생성한다.

glUseProgramStages()를 호출하여 프로그램을 어태치시켜 사용할 파이프라인의 섹션들을 구성한다.

// Create a vertex shader
GLuint vs = glCreateShader(GL_VERTEX_SHADER);

// Attach source and compile
glShaderSource(vs, 1, vs_source, NULL);
glCompileShader(vs);

// Create a program for our vertex stage and attach the vertex shader to
it
GLuint vs_program = glCreateProgram();
glAttachShader(vs_program, vs);

// Important part - set the GL_PROGRAM_SEPARABLE flag to GL_TRUE *then*
link
glProgramParameteri(vs_program, GL_PROGRAM_SEPARABLE, GL_TRUE);
glLinkProgram(vs_program);

// Now do the same with a fragment shader
GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, fs_source, NULL);
glCompileShader(fs);
GLuint fs_program = glCreateProgram();
glAttachShader(fs_program, vs);
glProgramParameteri(fs_program, GL_PROGRAM_SEPARABLE, GL_TRUE);
glLinkProgram(fs_program);

// The program pipeline represents the collection of programs in use:
// Generate the name for it here.
GLuint program_pipeline;
glGenProgramPipelines(1, &program_pipeline);

// Now use the vertex shader from the first program and the fragment
shader

// from the second program.
glUseProgramStages(program_pipeline, GL_VERTEX_SHADER_BIT, vs_program);
glUseProgramStages(program_pipeline, GL_FRAGMENT_SHADER_BIT, fs_program);

위 예제코드에서는 각각 하나의 쉐이더만 사용하는 두 개의 프로그램 객체를 사용하였다. 

GLuint glCreateShaderProgramv(GLenum type,
    GLsizei count,
    const char ** strings);

위 함수를 통해 한 프로그램 객체에 하나의 쉐이더 객체만 들어있는 간단한 프로그램 객체를 생성할 수 있다.

void glBindProgramPipeline(GLuint pipeline);

 

위 함수를 통해 여러 쉐이더 스테이지를 가진 프로그램 파이프라인 객체가 프로그램 객체로 컴파일되어 현재 파이프라인으로 설정가능하다.

Interface Matching

GLSL에는 한 쉐이더 스테이지 출력이 다음 스테이지 입력과 매칭되는지에 대한 규칙이 있다. 일련의 쉐이더를 모두 하나의 프로그램 객체에 링크시킬 때, 제대로 매칭되지 않으면 opengl의 링커가 알려준다. 

 각 스테이지에 대해 분리 프로그램 객체를 사용하면 프로그램 객체 교체시 매칭이 일어나지 않으므로 제대로 연결되지 않으면 프로그램에 사소한 오류가 생기거나 동작하지 않을 수 있다. 분리 프로그램 객체를 사용할 때에는 이를 유의하자.

 

쉐이더 스테이지의 출력 변수는 이름과 타입이 동일한 경우 다음 스테이지의 입력에 연결된다. 변수의 경우 다른 조건도 매칭되어야 한다. 인터페이스 블록의 경우, 인터페이스 두 블록은 동일한 이름과 동일한 순서의 멤버를 가져야한다. 이는 구조체도 비슷하다. 만약 인터페이스 변수가 배열이면, 양쪽 인터페이스는 동일한 개수의 배열 요소로 선언해야한다. 

 예외가 있다면 테셀레이션 및 지오메트리 쉐이더의 입출력이다. 이는 한 요소에서 배열로 변경이 가능하다.

 

만약 여러 스테이지에 대한 쉐이더들을 모두 하나의 프로그램 객체에 링크시키면, opengl은 인터페이스 멤버가 불필요함을 알고 쉐이더에서 제거한다. 

 

애플리케이션의 모든 쉐이더에서 입력/출력 변수들의 이름을 동일하게 맞추는 작업은 중요하다. 쉐이더 개수가 늘어나거나 더 많은 개발자가 쉐이더 작업을 하게 된다면 더 중요해진다. 레이아웃(layout) 지시어를 사용하면 여러 쉐이더 안에서 각 입력과 출력의 위치 지정은 가능하다. 

 

void glGetProgramInterfaceiv(GLuint program,
    GLenum programInterface,
    GLenum pname,
    GLint * params);

void glGetProgramResourceiv(GLuint program,
    GLenum programInterface,
    GLuint index,
    GLsizei propCount,
    const Glenum * props,
    GLsizei bufSize,
    GLsizei * length,
    GLint * params);

위 두 함수를 이용해서 프로그램 객체의 입출력 인터페이스를 질의할 수 있다.

 

void glGetProgramResourceName(GLuint program,
    GLenum programInterface,
    GLuint index,
    GLsizei bufSize,
    GLsizei * length,
    char * name);

위 함수를 호출하면 입력 또는 출력의 이름을 알 수 있다.

 

// Get the number of outputs
GLint outputs;
glGetProgramInterfaceiv(program, GL_PROGRAM_OUTPUT,
	GL_ACTIVE_RESOURCES, &outputs);
    
// A list of tokens describing the properties we wish to query
static const GLenum props[] = { GL_TYPE, GL_LOCATION };

// Various local variables
GLint i;
GLint params[2];
GLchar name[64];
const char * type_name;

for (i = 0; i < outputs; i++)
{
    // Get the name of the output
    glGetProgramResourceName(program, GL_PROGRAM_OUTPUT, i,
    	sizeof(name), NULL, name);
        
    // Get other properties of the output
    glGetProgramResourceiv(program, GL_PROGRAM_OUTPUT, i,
    	2, props, 2, NULL, params);
        
    // type_to_name() is a function that returns the GLSL name of
    // type given its enumerant value
    type_name = type_to_name(params[0]);
    
    // Print the result
    printf("Index %d: %s %s @ location %d.\n",
    	i, type_name, name, params[1]);
}

위 코드는 프로그램 객체의 활성화된 출력에 대한 정보를 출력한다. 

out vec4 color;
layout (location = 2) out ivec2 data;
out float extra;

만약 위와 같이 프래그먼트 쉐이더 코드를 사용하면

Index 0: vec4 color @ location 0.
Index 1: ivec2 data @ location 2.
Index 2: float extra @ location 1.

이러한 결과를 출력한다.

활성화된 출력들의 목록이 선언 순서대로 표시된다. 위 코드를 이용해서 출력의 타입을 확인할 수도 있다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유