본문 바로가기

Game/Graphics

Learn OpenGL - Getting started : Shaders

link : https://learnopengl.com/Getting-started/Shaders


Shaders


 Hello Triangle 튜토리얼에서 말했듯이 쉐이더는 GPU에 거의 의존하지 않는다.


이러한 프로그램은 그래픽 파이프 라인의 특정 섹션마다 실행된다.


기본적으로 쉐이더는 입력을 출력으로 변환하는 프로그렘에 지나지 않는다. 쉐이더는 서로 통신할 수 없기


때문에 매우 고립되있다. 그들이 가진 유일한 커뮤니케이션은 입력과 출력을 통해서이다.



 이전 튜토리얼에서 우리는 쉐이더의 표면과 그것들을 적절하게 사용하는 방법을 간략하게 설명했다.


보다 일반적인 방식으로 쉐이더, 특히 OpenGL 쉐이딩 언어를 설명할 것이다.





GLSL


 쉐이더는 C와 유사한 언어 GLSL로 작성된다. GLSL은 그래픽과 함께 사용하도록 설계되었으며


벡터 및 행렬 조작을 특별히 목표로 하는 유용한 기능을 포함한다.



 쉐이더는 항상 버전 선언부터 시작해 입력 및 출력 변수, 유니폼 및 주요 기능 목록이 뒤따른다.


각 쉐이더의 진입점은 입력 변수를 처리하고 출력 변수에 결과를 출력하는 main function이다.


유니폼이 무엇인지 알지 못한다고 걱정하지 말아라, 곧 알려주겠다.



 쉐이더는 일반적으로 다음과 같은 구조를 가진다:


#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;
}

 정점 쉐이더에 대해 구체적으로 이야기할 때 각 입력 변수는 정점 애트리뷰트라고도 한다.


우리가 하드웨어에 의해 제한을 선언 할 수 있는 최대의 정점 속성이 있다. OpenGL은 항상 최소한 16개의


4 컴포넌트 정점 애트리뷰트가 있음을 보장하지만 일부 하드웨어에서는 GL_MAX_VERTEX_ATTRIBS를


쿼리해 검색 할 수 있는 더 많은 것을 허용할 수 있다:


int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

대부분의 경우에는 16이상이면 충분하다.




Types


 GLSL은 어떤 종류의 변수를 처리할지 지정하기 위한 다른 프로그래밍 언어 데이터 유형과 같다.


GLSL은 C : int, float, double, uint 및 bool과 같은 언어에서 우리가 아는 기본 유형을 대부분 가지고 있다.


GLSL에는 또한 튜토리얼 전반에 걸쳐 벡터와 행렬을 많이 사용하게 될 두 가지 컨테이너 유형이 있따.


행렬에 대해서는 나중에 튜토리얼에서 설명할 것이다.




Vectors


 GLSL의 벡터는 방금 언급한 기본 유형의 1,2,3 또는 4 구성 요소 컨테이너이다.


그들은 다음 형식을 취할 수 있다. (n은 구성 요소의 수를 나타냄)



    - vecn : n floats의 기본 벡터


    - bvecn : n booleans의 벡터


    - ivecn : n integers의 벡터


    - uvecn : n unsigned integer의 벡터


    - dvecn : n double 컴포넌트의 벡터



벡터의 구성 요소는 vec.x를 통해 액세스 할 수 있다. (x를 .y .z .w를 사용해 액세스 할 수 있다)


GLSL을 사용하면 동일한 구성요소에 액세스해 색상에 rgba를 사용하거나 텍스처 좌표에 stpq를 사용할 수도 있다.


벡터 데이터 유형을 사용하면 Swizzling이라는 재미있고 유여한 구성 요소 선택이 가능하다.


Swizzling은 다음 구문을 허용한다.


vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

원본 벡터에 해당 구성 요소가 있는 한 최대 4자의 조합을 사용해 동일한 유형의 새 벡터를 만들 수 있다.


예를 들어 vec2의 .z 구성 요소에 액세스 할 수 없다. 벡터를 다른 벡터 생성자 호출에 인수로 전달해 필요한 인수의 수를


줄일 수 있다.


vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

따라서 벡터는 모든 종류의 입출력에 사용할 수 있는 유연한 데이터 유형이다.


튜토리얼 전반에 걸쳐 창조적으로 벡터를 관리하는 방법에 대한 많은 예제를 볼 수 있다.





Ins and outs


 쉐이더는 자체적으로 멋진 작은 프로그램이지만 그것들은 전체의 일부분이며, 우리가 개별 쉐이더에


입력과 출력을 갖고 싶기 때문에 우리가 stuff를 옮길 수 있다.


GLSL은 그 목적을 위해 특별히 in과 out키워드를 정의했다. 각 쉐이더는 해당 키워드와 출력 변수가 전달되는


다음 쉐이더 단계의 입력 변수와 일치하는 곳에서 입력 및 출력을 지정할 수 있다.


정점과 조각 쉐이더는 약간 다르다.



 정점 쉐이더는 어떤 형태의 입력을 받아야한다. 그렇지 않으면 매우 비효율적이다.


정점 쉐이더는 정점 데이터로부터 직접 입력을 받는 점에서 입력이 다르다. 정점 데이터의 구성 방법을


정의하기 위해 위치 메타 데이터를 사용해 입력 변수를 지정하므로 CPU에서 정점 속성을 구할 수 있다.


앞의 튜토리얼에서 이것을 layout(location = 0)으로 보았다. 따라서 정점 쉐이더는


입력에 대한 추가 레이아웃 사양을 요구하므로 이를 정점 데이터와 연결할 수 있다.

layout(location = 0) 지정자를 생략하고 glGetAttribLocation을 통해 OpenGL 코드에서 속성 위치를 쿼리하는 것도 가능하지만 정점 쉐이더에서 설정하는 것을 선호한다. 이것이 더 이해하기 쉽고 일부 작업을 저장한다.

 또 다른 예외는 프래그먼트 쉐이더가 최종 출력 컬러를 생성해야하기 때문에 프래그먼트 쉐이더에 vec4 컬러 출력 변수가


필요하다는 것이다. 프래그먼트 쉐이더에서 출력 색상을 지정하지 않으면 OpenGL이 개체를 검정색(또는 흰색)으로


렌더링한다.



 따라서 하나의 쉐이더에서 다른 쉐이더로 데이터를 보내려면 송신 쉐이더의 출력과 수신 쉐이더의 비슷한 입력을 선언해야한다.


타입과 이름이 양쪽에서 같을 때 OpenGL은 그 변수들을 함께 링크할 것이고, 쉐이더간에 데이터를 보낼 수 있다.


실제로 어떻게 작동하는지 보여주기 위해 이전 튜토리얼의 쉐이더를 변경해 정점 쉐이더가 프래그먼트 쉐이더의


색상을 결정하도록 할 것이다.





Vertex shader


#version 330 core
layout (location = 0) in vec3 aPos; // the position variable has attribute position 0
  
out vec4 vertexColor; // specify a color output to the fragment shader

void main()
{
    gl_Position = vec4(aPos, 1.0); // see how we directly give a vec3 to vec4's constructor
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark-red color
}


Fragment shader


#version 330 core
out vec4 FragColor;
  
in vec4 vertexColor; // the input variable from the vertex shader (same name and same type)  

void main()
{
    FragColor = vertexColor;
} 

 vertexColor 변수를 vertex shader에서 설정한 vec4 출력으로 선언하고, fragment shader에서 유사한


vertexColor 입력을 선언한 것을 볼 수 있다. 둘 다 유형과 이름이 같으므로 프래그먼트 쉐이더의 vertexColor는


정점 쉐이더이 vertexColor로 연결된다. 정점 쉐이더에서 색상을 진한 빨간색으로 설정했기 때문에


결과로 생성되는 부분도 진한 빨간색이어야한다. 다음 그림은 출력을 보여준다.



우리는 방금 정점 쉐이더에서 프래그먼트 쉐이더로 값을 보낼 수 있었다.





Uniforms


 유니폼은 CPU의 응용 프로그램에서 GPU의 쉐이더로 데이터를 전달하는 또 다른 방법이지만,


유니폼은 정점 속성과 약간 다르다. 우선 유니폼은 global하다.


전역 변수는 유니폼 변수가 쉐이더 프로그램 개체마다 고유하며


쉐이더 프로그램의 모든 단계에서 모든 쉐이더에서 액세스 할 수 있음을 의미한다.


둘째, 균일한 값을 설정하면 유니폼은 재설정되거나 업데이트 될 때까지 값을 유지한다.



 GLSL에서 유니폼을 선언하려면 유니폼 키워드를 유형 및 이름이 있는 쉐이더에 추가하기만 하면 된다.


그 시점부터 새로 선언된 유니폼을 쉐이더에서 사용할 수 있다. 이번에는 유니폼을 통해 삼각형의 색상을


설정할 수 있는지 보겠다:


#version 330 core
out vec4 FragColor;
  
uniform vec4 ourColor; // we set this variable in the OpenGL code.

void main()
{
    FragColor = ourColor;
}   

 우리는 프래그먼트 쉐이더에서 uniform vec4 ourColor를 선언하고 프래그먼트의 출력 색상을


이 균일한 값의 내용으로 설정했다. 유니폼은 전역 변수이기 때문에 원하는 모든 쉐이더에서


이를 정의할 수 있으므로 무언가를 프래그먼트 쉐이더로 가져오기 위해 정점 쉐이더를


다시 거칠 필요가 없다. 우리는 이 유니폼을 정점 쉐이더에서 사용하지 않으므로 이를 정의할 필요가 없다.

GLSL 코드에서 사용되지 않은 유니폼을 선언하면 컴파일러는 컴파일된 버전에서 자동으로 변수를 제거해 몇 가지 실망스러운 오류를 일으킨다. 이것을 명심해라!

 유니폼은 현재 비어있다. 우리는 아직 유니폼에 어떤 데이터도 추가하지 않았다. 그래서 시도해보아라.


먼저 쉐이더에서 유니폼 속성의 index/location을 찾아야한다. 유니폼의 index/location이 있으면


값을 업데이트 할 수 있다. 프래그먼트 쉐이더에 단일 색상을 전달하는 대신 점차 시간이 지남에


따라 점차 색상을 변경해 양념을 쳐보자:


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);


 먼저 glGetTime()을 통해 실행 시간을 초 단위로 가져온다. 그런 다음 sin 함수를 사용해 0.0-1.0 범위의 색상을


변경하고 greenValue에 결과를 저장한다.



 그런 다음 glGetUniformLocation을 사용해 ourColor 유니폼의 위치를 쿼리한다. 쉐이더 프로그램과


유니폼의 이름 (위치를 검색하고자 하는)을 쿼리 함수에 제공한다.


glGetUniformLocation이 -1을 반환하면 위치를 찾을 수 없다. 마지막으로 glUniform4f 함수를 사용해


uniform 값을 설정할 수 있다. 유니폼 위치를 찾는데에는 먼저 쉐이더 프로그램을 사용할 필요가


없지만 유니폼을 업데이트하려면 현재 활성화된 쉐이더 프로그램에서 유니폼을 설정해야하기 때문에


glUseProgram을 호출해 프로그램을 먼저 사용해야한다.

OpenGL은 C 라이브러리의 핵심이기 때문에 type 오버로딩을 기본적으로 지원하지 않으므로 다른 type으로 함수를 호출 할 수 있는 곳이면 어디서든 OpenGL은 필요한 각 type에 대해 새로운 함수를 정의한다. glUniform은 이것의 완벽한 예시이다. 이 함수는 설정하려는 유니폼의 유형에 특정 접미사가 필요하다. 가능한 접미어 중 일부는 다음과 같다.

- f : 함수가 float 값을 기대한다
- i : 함수가 int 값을 기대한다
- ui : 함수가 unsigned int 값을 기대한다
- 3f : 함수가 3개의 float 값을 기대한다
- fv : 함수가 float vector/array 값을 기대한다

OpenGL의 옵션을 구성하기를 원할 때마다 사용자의 유형에 해당하는 오버로드 된 함수를 선택하기만 하면 된다. 우리의 경우에는 유니폼 4개를 개별적으로 설정해 glUniform4f를 통해 데이터를 전달한다. (fv 버전을 사용할 수도 있다)

 이제 유니폼 변수 값을 설정하는 방법을 알았으니 이를 렌더링에 사용할 수 있다. 색상을 점차 변경하려면


게임 루프 반복마다 이 유니폼을 업데이트해야한다. 그렇지 않으면 삼각형은 단 한 번만


설정하고 단색을 유지한다. 그래서 우리는 greenValue를 계산하고 각 렌더 반복을 균일하게 업데이트한다.


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();
}

 이 코드는 이전 코드를 비교적 간단하게 적용한 것이다. 이번에는 삼각형을 그리기 전에 반복할 때마다


균일한 값을 업데이트한다. 유니폼을 올바르게 업데이트하면 삼각형의 색상이 녹색에서 검은색으로 점차 변하고


녹색으로 바뀌는 것을 볼 수 있다.





More attributes!


 우리는 이전 튜토리얼에서 VBO를 채우고 정점 속성 포인터를 구성해 VAO에 저장하는 방법을 보았다.


이번에는 정점 데이터에 색상 데이터를 추가하려고 한다. 우리는 정점 배열에 3개의 float로 색상 데이터를 추가할 것이다.


삼각형의 각 모서리에 각각 빨강, 녹색 및 파랑 색을 지정한다.


float vertices[] = {
    // positions         // colors
     0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // bottom right
    -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // bottom left
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // top 
};    

 이제 우리는 정점 쉐이더에 보낼 데이터가 더 많으므로 정점 쉐이더를 조정해


우리의 컬러 값을 정점 속성 입력으로 받아들여야한다. 레이아웃 지정자를 사용해 aColor 속성의 위치를 1로 설정한다.


#version 330 core
layout (location = 0) in vec3 aPos;   // the position variable has attribute position 0
layout (location = 1) in vec3 aColor; // the color variable has attribute position 1
  
out vec3 ourColor; // output a color to the fragment shader

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor; // set ourColor to the input color we got from the vertex data
}       

 우리는 더 이상 조각의 색상에 대해 유니폼을 사용하지 않지만 이제는 ourColor 출력 변수를 사용하기 때문에


프래그먼트 쉐이더도 변경해야한다.


#version 330 core
out vec4 FragColor;  
in vec3 ourColor;
  
void main()
{
    FragColor = vec4(ourColor, 1.0);
}

다른 정점 속성을 추가하고 VBO의 메모리를 업데이트했기 때문에 정점 속성 포인터를 다시 구성해야한다.


VBO 메모리의 업데이트된 데이터는 이제 다음과 같이 보인다.


Interleaved data of position and color within VBO to be configured wtih <function id='30'>glVertexAttribPointer</function>


 현재 레이아웃을 알고 있으면 glVertexAttribPointer로 정점 포맷을 업데이트 할 수 있다.


// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// color attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);

 glVertexAttribPointer의 처음 몇 가지 인수는 비교적 간단하다. 이번에는 속성 위치 1에 정점 속성을


구성하려고한다. 색상 값의 크기는 3개의 부동 소수점이며 값을 표준화하지 않는다.



 이제 두 개의 정점 속성이 있으므로 stride 값을 다시 계산해야한다. 데이터 배열에서 다음 속성 값을 얻으려면


오른쪽으로 6개, position 값으로 3개 color 값으로 3개 이동해야한다. 이것은 float의 크기를 바이트(24byte)의


6배로 늘릴 수 있다. 또한, 이번에는 오프셋을 지정해야한다. 각 정점에 대해 위치 정점 속성이


가장 먼저이므로 오프셋을 0으로 선언한다. 색상 속성은 위치 데이터 뒤에 시작되므로 오프셋은 바이트


단위로 3*sizeof(float)이다. (=12byte)


응용 프로그램을 실행하면 다음 이미지가 나타난다:






Our own shader class


 쉐이더를 작성, 컴파일 및 관리하는 것은 상당히 번거로운 일일수 있따. 쉐이더 제목에 대한 마지막 터치로서


디스크에서 쉐이더를 읽고 컴파일 및 링크하고 오류를 확인하며 사용하기 쉬운 쉐이더 클래스를 작성해


일상 생활을 좀 더 쉽게 할 수 있다. 이것은 또한 우리가 지금까지 배운 지식 중 일부를 유용한 추상적인


객체로 캡슐화 할 수 있는 아이디어를 제공한다.



 주로 학습 목적과 이식성을 위해 헤더 파일에 쉐이더 클래스를 작성하자. 필요한 include를 추가하고 클래스 구조를


정의해 시작하자:


#ifndef SHADER_H
#define SHADER_H

#include <glad/glad.h> // include glad to get all the required OpenGL headers
  
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
  

class Shader
{
public:
    // the program ID
    unsigned int ID;
  
    // constructor reads and builds the shader
    Shader(const GLchar* vertexPath, const GLchar* fragmentPath);
    // use/activate the shader
    void use();
    // utility uniform functions
    void setBool(const std::string &name, bool value) const;  
    void setInt(const std::string &name, int value) const;   
    void setFloat(const std::string &name, float value) const;
};
  
#endif

우리는 헤더 파일 맨 위에 몇 가지 전처리기 지시문을 사용했다. 이 작은코드행을 사용하면 여러 파일에 쉐이더 헤더가 포함되어 있어도 아직 포함되지 않은 경우에만 이 헤더 파일을 포함하고 컴파일하도록 컴파일러에 알린다. 이렇게하면 연결 충돌을 방지할 수 있다.

 쉐이더 클래스는 쉐이더 프로그램의 ID를 보유한다. 생성자는 디스크에 간단한 텍스트 파일로 저장할 수 있는 


꼭짓점 및 조각 쉐이더의 소스 코드 파일 경로가 필요하다. 약간의 추가 작업을 추가하기 위해 우리의 수명을 약간


줄이기 위해 몇 가지 유틸리티 함수를 추가한다. use는 쉐이더 프로그램을 활성화하고 모든 set... ㅎ마수는


균일한 위치를 쿼리하고 그 값을 설정한다.




Reading from file


 우리는 C++ 파일 스트림을 사용해 파일의 내용을 여러 문자열 객체로 읽는다.


Shader(const char* vertexPath, const char* fragmentPath)
{
    // 1. retrieve the vertex/fragment source code from filePath
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;
    // ensure ifstream objects can throw exceptions:
    vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
    fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
    try 
    {
        // open files
        vShaderFile.open(vertexPath);
        fShaderFile.open(fragmentPath);
        std::stringstream vShaderStream, fShaderStream;
        // read file's buffer contents into streams
        vShaderStream << vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();		
        // close file handlers
        vShaderFile.close();
        fShaderFile.close();
        // convert stream into string
        vertexCode   = vShaderStream.str();
        fragmentCode = fShaderStream.str();		
    }
    catch(std::ifstream::failure e)
    {
        std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
    }
    const char* vShaderCode = vertexCode.c_str();
    const char* fShaderCode = fragmentCode.c_str();
    [...]

 다음으로 쉐이더를 컴파일하고 링크해야한다. compilation/linking이 실패한 경우에도 검토 중이며,


그렇다면 디버깅 할 때 매우 유용한 컴파일 타임 오류를 인쇄하십시오.


// 2. compile shaders
unsigned int vertex, fragment;
int success;
char infoLog[512];
   
// vertex Shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// print compile errors if any
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if(!success)
{
    glGetShaderInfoLog(vertex, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};
  
// similiar for Fragment Shader
[...]
  
// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
// print linking errors if any
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if(!success)
{
    glGetProgramInfoLog(ID, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
  
// delete the shaders as they're linked into our program now and no longer necessery
glDeleteShader(vertex);
glDeleteShader(fragment);

use 함수는 간단하다:


void use() 
{ 
    glUseProgram(ID);
}  

유사하게 균일한 setter 함수들에 대해서:

:


void setBool(const std::string &name, bool value) const
{         
    glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); 
}
void setInt(const std::string &name, int value) const
{ 
    glUniform1i(glGetUniformLocation(ID, name.c_str()), value); 
}
void setFloat(const std::string &name, float value) const
{ 
    glUniform1f(glGetUniformLocation(ID, name.c_str()), value); 
} 

 그리고 거기에는 완성된 쉐이더 클래스가 있습니다. 쉐이더 클래스를 사용하는 것은 매우 쉽습니다.


한 번 쉐이더 객체를 만들고 간단히 사용하기 시작한다:


Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");
...
while(...)
{
    ourShader.use();
    ourShader.setFloat("someUniform", 1.0f);
    DrawStuff();
}

 여기서 우리는 shader.vs 및 shader.fs라는 두 파일에 정점 및 조각 쉐이더 소스 코드를 저장했다.


원하는대로 쉐이더 파일의 이름을 자유롭게 지정할 수 있습니다. 개인적으로 .vs 및 .fs 확장자는 매우 직관적이다.



 새롭게 생성된 쉐이더 클래스를 사용해 소스 코드를 찾을 수 있다. 쉐이더 파일 경로를 클릭해 각 쉐이더의


소스 코드를 찾을 수 있습니다.





** ** ** ** ** ** ** ** ** **


이번 강의도 기나긴 여정이었다. 이 튜토리얼이 정말 좋다고 느끼고 있는게 이미 사용해보았던 코드와


흐름이지만 정확하게 짚고 넘어가지 못했던 부분을 모두 상세하게 알려준다. 여러번 반복해서 공부했기 때문에


이렇게 느낄 수도 있겠지만 내 지식이 되어가는 느낌을 받으면서 공부를 하고 있다.


내일은 물놀이를 가야하니 이제 알고리즘 공부를 끝마쳐야겠다. 퐈이팅~~