WebGL Shader에 대해 살펴보기

하늘·2024년 1월 23일
0

webgl

목록 보기
2/2
post-thumbnail

Three.js나 Canvas를 공부를 하시는 분들이라면 WebGL은 당연히 들어보셨을 거라고 생각됩니다. 하지만 조금 더 깊게 공부하시면 Shader에 대해서 한 번 정도는 들으셨을 텐데, 이 Shader가 대체 정확히 무엇인지! 그리고 저는 three.js에서 Shader를 공부하기 전에 바닐라 자바스크립트로 구현하여 보는 게 더 이해가 되어 포스팅을 작성하게 되었습니다.

참고로 이 포스트는 Fastcampus 초격차 패키지 : 21개 프로젝트로 완성하는 인터랙티브 웹 개발 with Three.js & Canvas 강의 내용을 토대로 설명을 하고 있습니다.

먼저 Shader에 대해 간단히 설명하자면, 그래픽 하드웨어의 렌더링 데이터를 계산하는 데에 사용되는 함수입니다. 쉽게 말해서 어떠한 오브젝트에 위치, 색상, 투명도, transform 등 여러 요소를 줄 수 있는 방법을 말합니다.

마우스를 움직이면 마우스를 주위로 왜곡되어 보이는 현상도 Shader를 통해 구현한 것입니다.

shader에는 두 가지 shader를 중점적으로 볼 텐데,
첫 번째로 위치, 좌표를 그리는 Vertex Shader(정점 셰이더)와 두 번째로는 정점 셰이더로 그린 좌표를 토대로 효과를 줄 수 있는 Fragment Shader(색상 셰이더)가 있습니다.

Canvas 불러오기

const canvas = document.querySelector("canvas");

canvas.width = 300;
canvas.height = 300;

const gl = canvas.getContext("webgl");

먼저 html에 작성된 canvas tag를 자바스크립트로 끌고 와 주고, 크기를 지정한 다음 canvas에게 "너한테 이제 webgl 기술을 적용할 거야" 라고 getContext로 알려 줍니다.
그리고 그 기술을 gl이라는 변수에 담습니다.

Vertex Shader, Fragment Shader 생성

const vertexShader = gl.createShader(gl.VERTEX_SHADER)
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)

이제 gl에게 vertex shader, fragment shader 두 셰이더를 만들 거라고 알려 준 다음, 그걸 변수에 저장해 줍니다.

이제부터 중요한 부분인데, 먼저 vertex shader에 효과를 입힐 공간을(정점 데이터를) 알려 줘야 하는데, 우리가 아는 일반적인 자바스크립트 코드가 아닌 C언어 기반으로 된 webGL이 이해할 수 있는 언어로 코드를 작성해야 합니다.

C언어요? 저는 자바스크립트밖에 모릅니다만..
저도 지레 겁을 먹었지만 크게 어려운 부분이 없었고, 사용하다 보면 반복되는 것들이 많았습니다!

gl.shaderSource(vertexShader, "...여기에 셰이더 코드를 작성해 보아요...");

shader에 코드를 작성할 건데, 어떤 shader에 작성할 건지 알려 주고 두 번째 인자로는 어떻게 작성할 건지 알려 주면 됩니다.

gl.shaderSource(fragmentShader, "...여기에 셰이더 코드를 작성해 보아요...");

fragment Shader에도 동일하게 어떤 코드를 작성할 건지 알려 주어야 합니다.

그리고 코드를 다 작성했다고 webGL에게 알려 주어야 합니다.

gl.compileShader(vertexShader);
gl.compileShader(fragmentShader);

일단은 여기까지만 적어 주고 다음 스텝으로 넘어가 봅시다.
대신 여기에서 저장을 하면 당연히 셰이더 코드를 작성하지 않았으니 에러가 뜹니다.

vertexShader, FragmentShader 연결시켜 주기

각각의 셰이더를 작성했는데, 이거를 하나로 합쳐야 하는 과정이 필요합니다.

const program = gl.createProgram(); // 프로그램을 만들고

이제 program이라는 변수를 통해서 연결시켜 주면 됩니다.

gl.attachShader(program, vertexShader); // program과 vertexshader 연결
gl.attachShader(program, fragmentShader); // program과 fragmentshader 연결

gl.linkProgram(program); // 두 셰이더를 하나로 합치기
gl.useProgram(program); // 하나로 합친 program을 사용하겠다고 명시

정점 데이터 선언해 주기

정점 셰이더인 vertex shader에게 데이터를 넘겨 주기 위해서는 buffer data로 넘겨 주어야 하는데, 이 buffer란 GPU 하드웨어에서 사용할 수 있는 데이터를 저장하는 메모리 영역이라고 생각하시면 됩니다. 한 마디로 gpu에게 webGL 쓸 거니까 여윳공간 만들어 놔! 라고 생각하시면 됩니다.

webGL에서는 vertex data의 위치, 색상, 텍스트 좌표 등을 사용하는 데에 사용됩니다.

const vertices = new Float32Array([
-1, -1, 
-1, 1, 
1, 1, 
-1, -1,
1, 1, 
1, -1
]);

좌표값이 어디에 있는지 정점 데이터를 선언해 주어야 합니다.

이렇게 사각형이 있고, 십자선이 있습니다. 십자선 가운데를 0으로 하면 오른쪽으로 한 칸은 x축으로 1, 왼쪽으로 한 칸은 x축으로 -1,
y축도 마찬가지로 위로는 1, 아래로는 -1입니다.

위에 설정해 둔 vertices에 따르면 삼각형 두 개가 이렇게 배치되어 있는 형태가 될 것입니다.

그리고 이제 gpu에서 사용할 수 있는 메모리 영역인 buffer를 만들어 주어야 합니다.

const vertexBuffer = gl.createBuffer(); // buffer 객체 생성
gl.bindBuffer(gl.ARRAY_BUFFER, vertexButter); 

이제 생성한 buffer를 gl에 연결시켜 주어야 합니다.
bindBuffer를 통해 첫 번째 인자로는 buffer의 유형, 두 번째 인자로는 만든 buffer를 넣어 주어야 합니다.
우리는 vertices를 통해 정점 데이터를 배열로 선언했으니 gl.ARRAY_BUFFER로 넣어 주어야 합니다.

gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

그리고 바인딩된 버퍼를 전송해야 합니다.
gl.bufferData(전송할 버퍼 유형(배열), 실제 전송할 데이터, GPU에서 데이터를 어떻게 관리할 건지)

세 번째로 들어가는 GPU에서 데이터를 어떻게 관리할 건지로 넣은 인자는, 데이터가 자주 변경되지 않고 그리기 목적으로 자주 사용하는 STATIC_DRAW로 넣겠습니다.

정점의 위치 계산 도와주기

vertex shader와 fragment shader를 하나로 합친 program 기억나시나요?
이제 이 program을 이용해 정점의 위치를 어떻게 계산할지 도와주어야 합니다.

const position = gl.getAttribLocation(program, "position")

program의 특정한 속성의 위치를 찾는 데에 사용되는 getAttribLocation 함수입니다. "position"은 program 내에 정의된 속성이고, 속성의 위치를 나타내는 정수를 반환해 줍니다.

이제 정점 속성에 대한 정보를 webGL에게 전달해야 합니다. 거의 다 왔어요.. 휴 ^^

gl.vertexAttribPointer(
  position, // 설정할 정점 속성의 위치
  2, // vertices을 두 개씩 끊어서 읽게
  gl.FLOAT, // 데이터 요소의 타입, 여기에서는 실수
  false, // 데이터를 정규화할지에 대한 여부, 이미 데이터가 적절한 형식이면 false,
  0, // 데이터 간의 간격, 연속적인 데이터라면 0
  0 // offset 설정, 처음부터 시작할 거라면 0
);

gl.enableVertexAttribArray(position) 
// 셰이더에서 이 위치의 정점 데이터를 사용할 준비가 되었다
// 렌더링할 때 필수적


gl.drawArrays( // 데이터(어레이)로 데이터 그릴 때
  gl.TRIANGLES, // 삼각형을 그리겠다
  0, // 0번째부터
  6, // 아이템이 6개가 있다
);

이렇게 vertex shader, fragment shader 두 shader를 gpu에게 전달해 주는 과정이 끝났습니다.

하지만 앞서 우리가 vertex shader와 fragment shader에게 셰이더 코드 작성을 하지 않았으니 다시 올라가야 합니다.

vertexshader, fragmentshader 코드 작성

vertexShader

gl.shaderSource(vertexShader, `

`)

두 번째 인자로 백틱을 열어 주어서 우리는 새로운 언어의 코드를 작성해야 합니다.

gl.shaderSource(vertexShader, `

	void main() {
    
    }

`)

void main에 실제로 shader에서 해야 할 것들을 알려 주어야 합니다.
우리는 vertex shader에게 shader가 적용될 position을 알려 주어야 하니 position을 정해 주어야 합니다.

gl.shaderSource(vertexShader, `

	attribute vec2 position; 
    // attribute에 있는 x, y 2개의 값을 가진 vec2로 position을 선언
    
    
	void main() {
    
    	gl_Position = vec4(position, 0.0, 1.0)
    
    }

`)

먼저 shader의 속성에 있는 position을 선언해 주어야 합니다. 자바스크립트처럼 const, let, var가 아니라서 이 부분부터 저는 멘붕의 연속이었습니다..

그리고 vec2는 우리가 2차원의 도형을 사용할 거기 때문에 x, y 좌표값만 필요하니 2개의 값을 가진 vec2로 선언해 줍니다.

그리고 gl_Position으로 shader가 적용될 좌표값을 설정해 주면 되는데요,

vec4로 x, y, z, w를 설정해 주면 됩니다. 우리는 2D를 만들 것이니 때문에 z는 필요하지 않아서 0.0으로 선언해 주고, w는 원근감인데요. 보통 2D를 만들 때는 1.0으로 선언해 주면 됩니다.

fragmentShader



gl.shaderSource(fragmentShader, `

  precision mediump float;
  // 색상과 조명 같은 값은 정확도와 성능을 조절하는 데에 영향을 끼침 
  // mediump => 중간 정도의 정밀도와 성능 제공, 보편적으로 사용

	void main() {
    	gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0)
    }

`)

fragmentShader에도 똑같이 void main() {}을 열어 주고 정점 데이터로 선언한 위치에 색상을 입혀 봅니다. 여기에서 vec4는 vertexshader와 다르게 색상을 나타나는 fragmentshade에서는 RGBA(레드, 그린, 블루, 알파(투명도))를 나타냅니다.

참고로 rgba에서는 255가 최대값인데 여기에서는 1이 최대값이어서 1 = 255로 생각하시면 됩니다.

vec4(1.0, 0.0, 0.0, 1.0) 이렇게 설정해 두었으니 레드만 활성화된 상태입니다.

custom shader

이제 우리 마음대로 커스텀 셰이더를 만들어 봅시다.
새로운 position이라는 변수를 만들고, 그걸 색상에 입혀 볼 건데요.

vertexshader안에 있는 position은 -1 ~ 1 사이값의 숫자를 가집니다. 어떻게 아냐고요?

const vertices = new Float32Array([-1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1]);

그렇게 설정했으니까요..! 만약 -2, -2, -2, 2... 이렇게 설정했으면 -2 ~ 2 사이값을 가지게 됩니다.

gl.shaderSource(vertexShader, `

	attribute vec2 position; 
    // attribute에 있는 x, y 2개의 값을 가진 vec2로 position을 선언
    
    varying vec2 vPosition;
    
    
    
	void main() {
    
    	gl_Position = vec4(position, 0.0, 1.0)
    
    }

`)

vPosition을 만들어 주는데, 이건 fragmentShader에 넘겨 줄 친구라서 앞에 varying을 붙여 줍니다. 어떠한 값을 fragmentshader로 줄 때는 반드시 varying을 써야 합니다.

gl.shaderSource(vertexShader, `

	attribute vec2 position; 
    // attribute에 있는 x, y 2개의 값을 가진 vec2로 position을 선언
    
    varying vec2 vPosition;
    
    
    
	void main() {
    	vec2 newPosition = (position + 1.0) / 2.0;
    	
        gl_Position = vec4(position, 0.0, 1.0)
        
        vPosition = newPosition;
    
    }

`)

newPosition을 만들어 줍니다. position이 -1에서 1 사이 값을 가지니, 여기에서 1.0을 더해 주면 0에서 2 사이값을 가지게 됩니다. 그거를 또 2로 나누면 0에서 1 사이값을 가지게 되지요.

그리고 vPosition = newPosition; 로 값을 할당해 준 다음,

gl.shaderSource(fragmentShader, `



varying vec2 vPosition;
// vertex shader에서 전달받은 vPosition

void main () {
  gl_FragColor = vec4(vPosition, 0.0, 1.0);
}
`)

fragmentshader에 varying으로 받아서 사용해 주시면 됩니다.

그러면 이런 그라데이션 효과가 나타날 텐데, 노란색은 초록색과 빨간색이 섞여져서 만든 색입니다.

신기하지 않나요? 그리고 너무 어렵습니다. 선언해야 할 변수와 함수들이 너무 많아요. 단순히 저런 그라데이션 하나 만든다고 50줄 이상의 코드를 작성했습니다. ㅜㅜ

three.js는 3D 오브젝트를 webGL에서 편하게 사용하게 도와주는 라이브러리입니다.
three.js에서 custom shader를 만드는 건 앞서 말씀드렸던 여러 변수와 함수 없이도 조금 더 간편하게 만들 수 있어요.

다음에는 제가 더 공부해서 three.js에서 custom shader를 만드는 방법으로 글을 쪄 오겠습니다.

이유운 강사님, 너무 감사드립니다. 목소리도 짱 좋으시고 설명도 엄청 쉽게 해 주셔서 덕분에 이해할 수 있었던 것 같아요. 따봉.

profile
아무튼 어찌저찌 하고 있습니다.... 🫠

0개의 댓글