* always thanks to https://learnopengl.com/
6. Shaders
- 이전 장에서 말했다 시피 shader는 GPU안에 위치하는 작은 프로그램이다.
- graphics pipeline의 특정 section에서 동작하며 사실상 하는 일은 input을 받아서 ouput으로 transforming하는게 전부이다.
6.1 GLSL
- shader를 작성하는데 사용되는 언어이다. graphics를 위해 맞춤 고안된 언어이며 matrix 조작과 vector에 특화되어있다.
- shader는 항상 version declaration으로 시작한다.
- 이어서 input, output, uniform 변수들이 정의되고 shader의 entry point인 main함수가 있다.
- main에서 input을 process해서 output을 만들어낸다.
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
void main()
{
// process input(s) and do some weird graphics stuff
...
// output processed stuff to output variable
out_variable_name = weird_stuff_we_processed;
}
- vertex shader를 예로 설명을 한다면, input variable이라는 건 vertex attribute이라고도 한다.
- 하드웨어에 따라서 정의할 수 있는 attributes가 제한되어있다.
- maximum number of vertex attributes를 확인하는 방법은 아래와 같다.
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
6.2 Types
- GLSL가 가지고 있는 타입
- int, float, double, uint and bool
- vectors and matrices
6.2.1 Vectors
- component 크기(1,2,3,4)별로 있음. (x), (x,y), (x,y,z), (x,y,z,w)
- vec(n): the default vector of (n floats.)
- bvec(n): a vector of (n booleans.)
- ivec(n): a vector of (n integers.)
- uvec(n): a vector of (n unsigned integers.)
- dvec(n): a vector of (n double components.)
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
- vec2, vec4 처럼 component 수에 따른 타입으로 변수 정의
- 각 컴포넌트에 접근할때 .x 처럼 사용 가능
- .xxyz처럼 연속해서 컴포넌트를 접근할 수 있다.
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);
- xyzw대신 rgba(color), stpq(texture)로도 components에 접근이 가능하다.
- vec2의 경우 z는 없지만 vec4의 생성자에 vec2와 z,w를 넘겨서 vec4를 생성하는 위 문법도 가능하다.
6.3 Ins and outs
- shader는 하나의 작은 프로그램이자 전체의 한 부분이다. 따라서 각각의 shader들은 서로간 소통을 위해 input과 ouput을 갖는다.
- in, out은 그 목적을 위한 키워드이다. 각 shader의 output은 다음 shader의 input과 맞아야 한다.
- 단, vertex shader는 특별한 형태의 input을 받아야한다.
- vertex shader가 바로 vertex data를 받기 위해서는 vertex shader의 input 변수에 location metadata를 명시해주어야한다. 그래야 만이 vertex attributes를 cpu에게 알려줄 수 있다.
- 이전에 우리가 vertex shader를 만들때 layout (location = 0)을 적었던 것은 이 때문이다.
- 정리하면 vertex shader와 vertex data를 link하기 위해 vertex shader의 input에 대해서 layout specification을 해주어야 한다.
- fragment shader는 최종 output 색을 만들어내야 하므로 vec4 타입의 color output variable을 필요로한다.
- 위의 두 특수한 경우를 빼면 일반적으로 한 shader의 output은 다음 shader의 input과 비슷해야 한다.
- 따라서 두 변수의 type과 이름이 같다면 OpenGL이 알아서 그 변수들을 link해주고 이를 통해 shader간 데이터 전달이 가능하게 된다.
- 이전 삼각형 예제에서 vertex shader와 fragment shader를 한번 수정해보자.
#version 330 core
layout (location = 0) in vec3 aPos; // position has attribute position 0
out vec4 vertexColor; // specify a color output to the fragment shader
void main() {
gl_Position = vec4(aPos, 1.0); // we give a vec3 to vec4’s constructor
vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // output variable to dark-red
}
#version 330 core
out vec4 FragColor;
in vec4 vertexColor; // input variable from vs (same name and type)
void main() {
FragColor = vertexColor;
}
- vertex shader의 output은 vertexColor라는 vec4타입의 변수이다.
- fragment shader에는 위의 output을 받기 위해 input으로 vec4타입의 vertexColor를 정의했다.
- 서로 같은 타입과 이름이므로 이를 통해 데이터를 전달할 수 있다.
- vertex shader에서 dark-red컬러로 vertexColor를 내보내고 fragment shader가 이를 input으로 받아 dark-red를 내보내자.
6.4 Uniforms
- cpu에서 돌아가는 우리 application에서 GPU에 있는 shader로 데이터를 전달하는 또 다른 방법이다.
- uniform variable은 global(전역)으로 선언된다. 유일하며 모든 shader로부터 접근이 가능하다. 또한 uniform variable의 value를 바꾸기 전까지 계속해서 유지된다. 전역변수의 성질과 비슷하다.
- 이번에는 삼각형의 색을 uniform변수를 통해 바꾸어보자.
version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // we set this variable in the OpenGL code.
void main() {
FragColor = ourColor;
}
- fragment shader에 uniform으로 vec4타입의 변수 ourColor를 선언하였고 output fragColor를 ourColor로 set했다.
- uniforms가 global 변수이기 때문에 어느 shader에서 접근이 가능하다. 따라서 fragment의 색을 바꾸기 위해서 다시 vertex shader로 들어가서 color를 바꾸고 다른 shader로 보내주지 않아도 된다.
- vertex shader에는 uniform을 사용하지 않기 때문에 이곳에 uniform을 정의할 필요가 없다.
- 자 그럼 다시 코드로 돌아가서 uniform을 보자. uniform은 현재 비어있다.
- 이제 uniform에 값을 채워주어야 한다. 그럼 먼저 uniform attribute의 location/index를 알아야 할 것이다.
- 위치를 알았으면 uniform 변수에 값을 채워 줄 것 인데 이때 색을 시간에 따라 다양하게 넣어주자 아래 코드를 보자.
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
- glfwGetTime으로 실행시간(초)을 담는다.
- greenValue에 sin함수를 활용해서 0.0 ~ 1.0사이의 값을 담는다.
- glGetUniformLocation라는 함수로 ourColor라는 uniform변수의 location을 qurery한다. uniform 변수 이름과 shaderProgram을 넣어준다.
- 만약 glGetUniformLocation가 -1 을 반환하면 uniform변수의 위치를 못 찾은 것이다.
- 위치를 찾았으면 glUniform4f함수로 uniform변수의 값을 저장한다.
- 여기서 주의할 점은 uniform변수의 위치를 찾을 때와 달리 glUniform4f는 현재 활성화 된 shader program의 uniform변수를 update하기 때문에 glUserProgram을 먼저 호출해줘야 한다.
- OpenGL의 core는 C로 되어있기 때문에 함수 오버로딩을 지원하지 않는다. 따라서 glUniform을 호출할 때 후위식으로 타입을 표현해줘야 한다. glUniform4f는 네개의 float으로 uniform을 초기화 한다는 뜻이다.
- f : float
- i : int
- ui : unsigned int
- 3f : 3 floats
- fv : float vector or array
- 자 이제 rendering에 uniform variable을 사용해 보자.
- uniform변수에 실시간으로 값이 다르게 담길 수 있게 계산을 하도록 했다.
- 그럼 이 초기화 구문은 rendering이 반복될때마다 수행되어야 한다.
while(!glfwWindowShouldClose(window))
{
// input
processInput(window);
// render
// clear the colorbuffer
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// be sure to activate the shader
glUseProgram(shaderProgram);
// update the uniform color
float timeValue = glfwGetTime();
float greenValue = sin(timeValue) / 2.0f + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram,
"ourColor");
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
// now render the triangle
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
// swap buffers and poll IO events
glfwSwapBuffers(window);
glfwPollEvents();
}
- 매 frame마다 삼각형을 그리기 전에 uniform value를 update한다.
- 위의 내용을 조합해서 실행해보자
*전체코드
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <cmath>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
const char *vertexShaderSource ="#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos, 1.0);\n"
"}\0";
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"uniform vec4 ourColor;\n"
"void main()\n"
"{\n"
" FragColor = ourColor;\n"
"}\n\0";
int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// build and compile our shader program
// ------------------------------------
// vertex shader
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// check for shader compile errors
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// fragment shader
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// check for shader compile errors
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// link shaders
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// check for linking errors
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
float vertices[] = {
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
0.0f, 0.5f, 0.0f // top
};
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
// glBindVertexArray(0);
// bind the VAO (it was already bound, but just to demonstrate): seeing as we only have a single VAO we can
// just bind it beforehand before rendering the respective triangle; this is another approach.
glBindVertexArray(VAO);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// be sure to activate the shader before any calls to glUniform
glUseProgram(shaderProgram);
// update shader uniform
float timeValue = glfwGetTime();
float greenValue = sin(timeValue) / 2.0f + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
// render the triangle
glDrawArrays(GL_TRIANGLES, 0, 3);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
- uniform값이 매 frame마다 변하기 때문에 실행을 시켜보면 삼각형의 색이 계속 변함을 관찰할 수 있다.
- 즉 시간에 따라서 설정이 변하도록 할 때, application과 shader가 상호작용할 때 uniform은 상당히 유용하다.
- uniform 변수를 vertex수 만큼 정의하고 값을 대입 해주면 세 꼭지점 마다 다른 색으로 삼각형이 그려질 것이다. 하지만 이것보다 더 좋은 방법을 뒤에서 살펴보자.
'ComputerScience > Computer Graphics' 카테고리의 다른 글
CG - 1. 컴퓨터 그래픽스? (0) | 2021.09.02 |
---|---|
OpenGL - 6 Shaders (2) (0) | 2021.07.15 |
OpenGL - 5 Hello Triangle (3) (0) | 2021.06.30 |
OpenGL - 5 Hello Triangle (2) (0) | 2021.06.29 |
OpenGL - 5 Hello Triangle (1) (0) | 2021.06.29 |