WebGL 쉐이더 프로그래밍: GLSL 기초부터 고급 기법까지 🎨✨

콘텐츠 대표 이미지 - WebGL 쉐이더 프로그래밍: GLSL 기초부터 고급 기법까지 🎨✨

 

 

안녕하세요, 여러분! 오늘은 정말 흥미진진한 주제로 여러분과 함께할 거예요. 바로 WebGL 쉐이더 프로그래밍에 대해 깊이 파헤쳐볼 거랍니다. 😎 GLSL(OpenGL Shading Language)의 기초부터 고급 기법까지, 마치 카톡으로 수다 떨듯이 재미있게 설명해드릴게요. 그럼 준비되셨나요? 고고씽~ 🚀

잠깐! 혹시 여러분, 재능넷이라는 사이트 아세요? 거기서 다양한 재능을 거래할 수 있다던데... 어쩌면 우리가 오늘 배울 WebGL 쉐이더 프로그래밍 스킬도 나중에 재능넷에서 공유할 수 있을지도 몰라요! 🤔 자, 그럼 본격적으로 시작해볼까요?

1. WebGL과 GLSL: 기초 개념 잡기 🌈

자, 여러분! WebGL이 뭔지 아시나요? 간단히 말해서, WebGL은 웹 브라우저에서 3D 그래픽을 구현할 수 있게 해주는 JavaScript API예요. 그리고 GLSL은 이 WebGL에서 사용하는 쉐이딩 언어랍니다. 쉐이더가 뭐냐고요? 음... 쉐이더는 그래픽 카드에서 실행되는 작은 프로그램이라고 생각하면 돼요. 이 꼬마 프로그램들이 픽셀의 색상이나 위치를 결정하는 거죠. 😉

GLSL로 작성된 쉐이더는 크게 두 가지 종류가 있어요:

  • 버텍스 쉐이더 (Vertex Shader): 3D 공간에서 점들의 위치를 계산해요.
  • 프래그먼트 쉐이더 (Fragment Shader): 각 픽셀의 색상을 결정해요.

이 두 친구들이 협력해서 멋진 그래픽을 만들어내는 거예요. 마치 화가와 색칠하는 사람이 협력하는 것처럼요! 🎨

WebGL 렌더링 파이프라인 버텍스 쉐이더 래스터라이저 프래그먼트 쉐이더

위의 그림을 보면 WebGL의 렌더링 파이프라인을 한눈에 볼 수 있어요. 버텍스 쉐이더에서 시작해서 프래그먼트 쉐이더로 끝나는 과정이 보이시죠? 이게 바로 우리가 앞으로 다룰 내용의 핵심이에요! 👀

2. GLSL 문법: C 언어와 비슷하지만 다르다구요! 🤓

GLSL의 문법은 C 언어와 비슷해요. 하지만 그래픽스에 특화된 몇 가지 특별한 기능들이 있답니다. 예를 들어, vec2, vec3, vec4 같은 벡터 타입이나 mat2, mat3, mat4 같은 행렬 타입이 기본으로 제공돼요. 이런 타입들은 3D 그래픽스에서 정말 유용하게 쓰인답니다. 👍

간단한 GLSL 코드를 한번 볼까요?


// 버텍스 쉐이더 예제
attribute vec3 position;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

// 프래그먼트 쉐이더 예제
precision mediump float;

uniform vec3 color;

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

우와~ 뭔가 복잡해 보이죠? 하지만 걱정 마세요. 하나씩 차근차근 설명해드릴게요! 😊

버텍스 쉐이더에서는 position이라는 attribute를 받아서 변환 행렬을 적용한 후, gl_Position에 최종 위치를 저장해요. 여기서 gl_Position은 버텍스 쉐이더의 출력을 나타내는 특별한 변수예요.

프래그먼트 쉐이더에서는 color라는 uniform 변수를 받아서 gl_FragColor에 최종 색상을 저장해요. gl_FragColor는 프래그먼트 쉐이더의 출력을 나타내는 특별한 변수랍니다.

이렇게 GLSL에서는 특별한 내장 변수들을 사용해서 그래픽스 파이프라인과 소통한답니다. 신기하죠? 🌟

3. WebGL 컨텍스트 설정: 캔버스야, 준비됐니? 🎭

자, 이제 GLSL 코드를 작성했으니 이걸 어떻게 실행시킬 수 있을까요? 바로 여기서 WebGL 컨텍스트가 등장합니다! WebGL 컨텍스트는 HTML5 캔버스 요소에서 생성할 수 있어요. 한번 코드로 살펴볼까요?


// HTML
<canvas id="myCanvas" width="800" height="600"></canvas>

// JavaScript
const canvas = document.getElementById('myCanvas');
const gl = canvas.getContext('webgl');

if (!gl) {
    console.error('WebGL을 지원하지 않는 브라우저입니다. ㅠㅠ');
}

이렇게 하면 WebGL 컨텍스트가 생성돼요. 이제 이 gl 객체를 사용해서 WebGL 명령을 실행할 수 있답니다. 😎

그런데 잠깐, 여러분! 혹시 이런 생각 들지 않나요? "어... 이거 좀 복잡한데? 🤔" 맞아요, WebGL은 처음에는 좀 어려울 수 있어요. 하지만 걱정 마세요! 우리가 함께 하나씩 배워나가다 보면 어느새 WebGL 마스터가 되어 있을 거예요. 그리고 나중에는 이런 실력을 재능넷같은 플랫폼에서 다른 사람들과 공유할 수도 있겠죠? 멋지지 않나요? 😉

4. 쉐이더 컴파일과 프로그램 링크: 조립의 시간이에요! 🔧

자, 이제 우리가 작성한 GLSL 코드를 WebGL에서 사용할 수 있게 만들어볼 거예요. 이 과정은 마치 레고 블록을 조립하는 것과 비슷해요. 각각의 쉐이더를 만들고, 그것들을 하나의 프로그램으로 연결하는 거죠. 😊

먼저, 쉐이더를 컴파일해볼까요?


function compileShader(gl, type, source) {
    const shader = gl.createShader(type);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.error('쉐이더 컴파일 에러:', gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
    }
    
    return shader;
}

const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

우와~ 뭔가 대단해 보이죠? 😲 하지만 걱정 마세요. 이 코드는 그냥 우리가 작성한 GLSL 코드를 WebGL이 이해할 수 있는 형태로 변환하는 거예요. 마치 우리가 쓴 편지를 컴퓨터가 읽을 수 있는 언어로 번역하는 것과 비슷하답니다.

이제 컴파일된 쉐이더를 프로그램으로 링크해볼까요?


const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);

if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error('프로그램 링크 에러:', gl.getProgramInfoLog(program));
    return;
}

gl.useProgram(program);

짜잔~ 🎉 이제 우리의 쉐이더 프로그램이 완성됐어요! 이 프로그램을 사용해서 멋진 그래픽을 그릴 수 있게 됐답니다. 정말 신나지 않나요?

꿀팁! 쉐이더 컴파일과 링크 과정에서 에러가 발생하면 꼭 콘솔 로그를 확인해보세요. 대부분의 경우, 문법 오류나 타입 불일치 같은 간단한 실수 때문이에요. 디버깅할 때 정말 유용하답니다! 👍

5. 버퍼와 속성: 데이터를 GPU로 보내자! 📤

자, 이제 우리의 쉐이더 프로그램이 준비됐어요. 그런데 아직 뭔가 부족하지 않나요? 맞아요, 바로 데이터예요! 우리가 그리고 싶은 도형의 정보를 GPU에 전달해야 해요. 이때 사용하는 것이 바로 버퍼와 속성이랍니다. 😎

먼저 버퍼를 만들어볼까요?


const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

const positions = [
    -1.0, -1.0,
     1.0, -1.0,
    -1.0,  1.0,
     1.0,  1.0
];

gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

우와~ 뭔가 복잡해 보이죠? 하지만 걱정 마세요. 이건 그냥 우리가 그리고 싶은 사각형의 꼭지점 위치를 GPU에 전달하는 거예요. 마치 친구에게 "여기 여기 여기 여기에 점을 찍어줘"라고 말하는 것과 비슷하답니다. 😉

이제 이 데이터를 쉐이더의 속성과 연결해볼까요?


const positionAttributeLocation = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

이 코드는 우리가 만든 버퍼의 데이터를 쉐이더의 'position' 속성과 연결하는 거예요. 마치 "야, 이 데이터는 position이라는 이름의 속성이야. 잘 기억해!"라고 GPU에게 말해주는 것과 같답니다. 😄

알쏭달쏭 포인트! gl.vertexAttribPointer 함수의 매개변수들이 좀 헷갈리시나요? 걱정 마세요. 이 함수는 그냥 "이 데이터는 이런 모양이야"라고 GPU에게 설명해주는 거예요. 2는 각 꼭지점이 x와 y 두 개의 값을 가진다는 뜻이고, gl.FLOAT는 그 값들이 부동소수점 숫자라는 뜻이에요. 나머지 매개변수들은 지금은 신경 쓰지 않아도 돼요. 나중에 더 자세히 배울 거예요! 😉

6. Uniform 변수: 쉐이더에게 정보를 주자! 💌

자, 이제 우리의 쉐이더 프로그램이 데이터도 받을 준비가 됐어요. 그런데 가만히 생각해보니, 우리가 그리는 도형의 색상을 바꾸고 싶다면 어떻게 해야 할까요? 이럴 때 사용하는 것이 바로 Uniform 변수예요! 😃

Uniform 변수는 쉐이더 프로그램 실행 중에 변경할 수 있는 전역 변수 같은 거예요. 예를 들어, 색상이나 변환 행렬 같은 것들을 Uniform 변수로 전달할 수 있답니다.

한번 색상을 전달하는 Uniform 변수를 만들어볼까요?


const colorUniformLocation = gl.getUniformLocation(program, 'color');
gl.uniform3f(colorUniformLocation, 1.0, 0.0, 0.0);  // 빨간색!

우와~ 이제 우리의 도형이 빨간색으로 변했어요! 😲 이 코드는 쉐이더에게 "야, color라는 이름의 Uniform 변수가 있을 거야. 거기에 빨간색 정보를 넣어줄게!"라고 말하는 거예요.

그런데 잠깐, 여러분! 혹시 이런 생각 들지 않나요? "어... 이거 좀 신기한데? 🤔" 맞아요, WebGL과 GLSL을 사용하면 이렇게 동적으로 그래픽을 제어할 수 있어요. 이런 기술을 활용하면 정말 멋진 인터랙티브 웹 애플리케이션을 만들 수 있답니다. 어쩌면 나중에 여러분이 만든 멋진 WebGL 작품을 재능넷에서 공유할 수 있을지도 몰라요! 그렇게 되면 정말 멋지겠죠? 😉

7. 그리기 명령: 드디어 화면에 그려볼 시간이에요! 🖌️

자, 이제 모든 준비가 끝났어요! 우리의 쉐이더 프로그램도 있고, 데이터도 GPU에 전달했고, Uniform 변수로 색상도 설정했어요. 이제 남은 건 뭘까요? 바로 그리기 명령을 내리는 거예요! 😄

그리기 명령을 내리는 코드를 한번 볼까요?


gl.clearColor(0.0, 0.0, 0.0, 1.0);  // 검은색 배경
gl.clear(gl.COLOR_BUFFER_BIT);

gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

우와~ 이게 뭘까요? 😲 하나씩 설명해드릴게요:

  • gl.clearColorgl.clear: 이건 그냥 캔버스를 깨끗이 지우는 거예요. 마치 칠판을 지우는 것처럼요!
  • gl.drawArrays: 이게 바로 실제로 그리기를 시작하는 명령이에요. "자, 이제 그려!"라고 GPU에게 말하는 거죠.

gl.TRIANGLE_STRIP은 우리가 그리려는 도형의 타입을 나타내요. 이건 "점들을 이어서 삼각형을 계속 그려나가세요"라는 뜻이에요. 0은 시작 인덱스, 4는 그릴 꼭지점의 개수를 나타내죠.

이 명령을 실행하면... 짜잔! 🎉 우리의 캔버스에 빨간 사각형이 그려졌어요! 정말 신기하지 않나요?

WebGL로 그린 빨간 사각형 WebGL로 그린 빨간 사각형

여러분, 우리가 방금 WebGL을 사용해서 첫 번째 그래픽을 만들었어요! 👏👏👏 정말 대단하지 않나요? 이제 여러분은 WebGL의 기본적인 작동 원리를 이해하게 됐어요. 이걸 바탕으로 더 복잡하고 멋진 그래픽을 만들 수 있답니다!

꿀팁! WebGL은 성능이 정말 좋아요. 그래서 복잡한 3D 그래픽이나 애니메이션을 만들 때 자주 사용된답니다. 예를 들어, 웹 기반 게임이나 데이터 시각화 같은 분야에서 많이 쓰여요. 여러분도 이런 멋진 애플리케이션을 만들어볼 수 있을 거예요! 💪

8. 애니메이션: 움직임을 만들어볼까요? 🎬

자, 이제 우리는 정적인 사각형을 그릴 수 있게 됐어요. 근데 뭔가 좀 심심하지 않나요? 그래픽스의 진짜 재미는 바로 움직임에 있답니다! 그럼 우리의 사각형에 생명을 불어넣어볼까요? 😃

애니메이션을 만들기 위해서는 시간에 따라 변하는 값을 쉐이더에 전달해야 해요. 이를 위해 우리는 새로운 Uniform 변수를 사용할 거예요. 이 변수를 통해 시간 정보를 쉐이더에 전달할 수 있답니다.

먼저, 프래그먼트 쉐이더를 조금 수정해볼게요:


// 프래그먼트 쉐이더
precision mediump float;

uniform vec3 color;
uniform float time;

void main() {
    vec3 finalColor = vec3(
        sin(time) * 0.5 + 0.5,
        cos(time) * 0.5 + 0.5,
        sin(time + 3.14) * 0.5 + 0.5
    );
    gl_FragColor = vec4(finalColor, 1.0);
}

우와~ 이게 뭘까요? 😲 이 코드는 시간에 따라 색상이 변하도록 만들어요. sincos 함수를 사용해서 색상 값을 계산하고 있어요. 이렇게 하면 색상이 부드럽게 변화하게 됩니다.

이제 JavaScript에서 이 time 값을 계속 업데이트해주면 돼요:


const timeUniformLocation = gl.getUniformLocation(program, 'time');

function render(time) {
    time *= 0.001;  // 밀리초를 초로 변환

    gl.uniform1f(timeUniformLocation, time);

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

    requestAnimationFrame(render);
}

requestAnimationFrame(render);

짜잔~ 🎉 이제 우리의 사각형이 무지개 색으로 반짝반짝 빛나고 있어요! 정말 아름답지 않나요?

WebGL로 그린 무지개 색 사각형 WebGL로 그린 무지개 색 사 각형

여러분, 우리가 방금 만든 이 애니메이션은 정말 대단한 거예요! 😃 우리는 GPU의 힘을 빌려 매 프레임마다 복잡한 계산을 수행하고 있어요. 그런데도 부드럽게 움직이죠? 이게 바로 WebGL의 힘이랍니다!

꿀팁! 애니메이션을 만들 때는 requestAnimationFrame 함수를 사용하는 것이 좋아요. 이 함수는 브라우저의 리페인트 주기에 맞춰 콜백을 실행하기 때문에, 부드러운 애니메이션을 만들 수 있답니다. 또한, 탭이 비활성화될 때 자동으로 애니메이션을 멈춰서 리소스를 절약해준답니다. 👍

9. 텍스처: 이미지를 입혀볼까요? 🖼️

자, 이제 우리는 움직이는 사각형을 만들었어요. 근데 뭔가 좀 심심하지 않나요? 그래요, 단색 사각형보다는 이미지가 있는 게 더 재밌겠죠? 그럼 이제 텍스처를 사용해볼 거예요! 😃

텍스처란 간단히 말해서 이미지를 3D 객체에 입히는 기술이에요. 마치 선물 상자에 포장지를 붙이는 것처럼요! 🎁

먼저, 버텍스 쉐이더와 프래그먼트 쉐이더를 수정해볼게요:


// 버텍스 쉐이더
attribute vec4 a_position;
attribute vec2 a_texcoord;

varying vec2 v_texcoord;

void main() {
    gl_Position = a_position;
    v_texcoord = a_texcoord;
}

// 프래그먼트 쉐이더
precision mediump float;

varying vec2 v_texcoord;
uniform sampler2D u_texture;

void main() {
    gl_FragColor = texture2D(u_texture, v_texcoord);
}

우와~ 뭔가 복잡해 보이죠? 😲 하지만 걱정 마세요. 이건 그냥 "이미지의 어느 부분을 이 픽셀에 그릴까요?"라고 GPU에게 물어보는 거예요.

이제 JavaScript에서 텍스처를 로드하고 GPU에 전달해야 해요:


function loadTexture(gl, url) {
    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);

    // 텍스처가 로드되기 전까지 보여줄 임시 이미지
    const level = 0;
    const internalFormat = gl.RGBA;
    const width = 1;
    const height = 1;
    const border = 0;
    const srcFormat = gl.RGBA;
    const srcType = gl.UNSIGNED_BYTE;
    const pixel = new Uint8Array([255, 0, 255, 255]);  // 핑크색
    gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
                  width, height, border, srcFormat, srcType,
                  pixel);

    const image = new Image();
    image.onload = function() {
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
                      srcFormat, srcType, image);
        gl.generateMipmap(gl.TEXTURE_2D);
    };
    image.src = url;

    return texture;
}

const texture = loadTexture(gl, 'path/to/your/image.jpg');

짜잔~ 🎉 이제 우리의 사각형에 이미지가 입혀졌어요! 정말 멋지지 않나요?

WebGL로 그린 텍스처 사각형 WebGL로 그린 텍스처 사각형

여러분, 우리가 방금 만든 이 텍스처 매핑은 정말 대단한 거예요! 😃 이 기술을 사용하면 3D 게임의 캐릭터에 옷을 입히거나, 웹 기반 3D 모델링 툴에서 재질을 적용하는 등 다양한 응용이 가능해요.