콘텐츠 대표 이미지 - 자바스크립트 WebGL 셰이더 프로그래밍: 웹에서 3D 그래픽의 마법을 부리는 방법 🚀

자바스크립트 WebGL 셰이더 프로그래밍: 웹에서 3D 그래픽의 마법을 부리는 방법 🚀

안녕? 오늘은 웹 브라우저에서 화려한 3D 그래픽을 구현할 수 있는 자바스크립트 WebGL 셰이더 프로그래밍에 대해 함께 알아볼 거야. 🎮 게임, 🎨 인터랙티브 아트, 📊 데이터 시각화까지 - 웹에서 그래픽의 한계를 뛰어넘는 마법 같은 기술을 친구처럼 쉽게 설명해줄게!

요즘 웹 개발자들 사이에서 WebGL 기술이 엄청 핫해졌어. 재능넷에서도 WebGL 관련 재능 거래가 늘고 있다고 하더라고. 왜 그런지, 어떻게 시작하면 좋을지 함께 알아보자!

📚 목차

  1. WebGL과 셰이더의 기본 개념
  2. WebGL 렌더링 파이프라인 이해하기
  3. 첫 번째 WebGL 프로그램 만들기
  4. GLSL 셰이더 언어 기초
  5. 버텍스 셰이더와 프래그먼트 셰이더
  6. 텍스처와 라이팅 효과 구현하기
  7. 인터랙티브 효과와 애니메이션
  8. 성능 최적화 테크닉
  9. 실전 프로젝트: 인터랙티브 3D 모델 뷰어
  10. WebGL 셰이더의 미래와 발전 방향

1. WebGL과 셰이더의 기본 개념 🧩

WebGL이 뭔지 궁금하지? 간단히 말하면 웹 브라우저에서 GPU를 활용해 2D/3D 그래픽을 렌더링할 수 있게 해주는 JavaScript API야. 2011년에 첫 등장한 이후로 웹 그래픽의 판도를 완전히 바꿔놓았지.

WebGL의 핵심, 셰이더란? 🤔

셰이더(Shader)는 GPU에서 실행되는 작은 프로그램이야. 그래픽 렌더링 과정의 특정 단계를 프로그래밍 방식으로 제어할 수 있게 해주지. 마치 그래픽 카드에게 "이 픽셀은 이렇게 그려줘!"라고 직접 명령을 내리는 거라고 생각하면 돼.

셰이더는 크게 두 종류가 있어:

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

이 두 셰이더의 조합으로 거의 모든 그래픽 효과를 만들 수 있어. 마치 요리사가 재료와 레시피로 다양한 요리를 만들 수 있는 것처럼!

CPU JavaScript GPU Shaders WebGL 렌더링 결과

위 그림에서 볼 수 있듯이, JavaScript는 CPU에서 실행되고 셰이더는 GPU에서 실행돼. WebGL은 이 둘 사이의 다리 역할을 하는 거지. 이런 구조 덕분에 복잡한 3D 그래픽도 웹 브라우저에서 부드럽게 렌더링할 수 있어.

2. WebGL 렌더링 파이프라인 이해하기 🔄

WebGL이 어떻게 화면에 그래픽을 그리는지 그 과정을 알면 셰이더 프로그래밍이 훨씬 쉬워질 거야. 이 과정을 '렌더링 파이프라인'이라고 부르는데, 마치 공장의 조립 라인처럼 여러 단계로 이루어져 있어.

WebGL 렌더링 파이프라인의 주요 단계 🛠️

  1. JavaScript 코드 실행: 버퍼에 정점 데이터를 설정하고 셰이더 프로그램을 컴파일해.
  2. 버텍스 셰이더 실행: 각 정점의 위치를 계산하고 변환해.
  3. 프리미티브 조립: 정점들을 삼각형, 선, 점 등의 기본 도형으로 조합해.
  4. 래스터화: 3D 도형을 2D 픽셀로 변환해.
  5. 프래그먼트 셰이더 실행: 각 픽셀의 색상을 계산해.
  6. 프레임 버퍼 작업: 최종 이미지를 화면에 출력해.
JavaScript 버텍스 셰이더 프리미티브 조립 래스터화 프래그먼트 셰이더 프레임 버퍼 데이터 준비 정점 변환 삼각형 생성 픽셀 변환 색상 계산 화면 출력

이 파이프라인에서 우리가 직접 프로그래밍할 수 있는 부분은 버텍스 셰이더와 프래그먼트 셰이더야. 나머지 단계는 WebGL이 알아서 처리해주지. 셰이더 프로그래밍의 매력은 바로 이 두 단계에서 무한한 창의성을 발휘할 수 있다는 거야! 🎨

이 파이프라인을 이해하면 "아, 이 효과를 내려면 버텍스 셰이더에서 이렇게 해야겠구나" 또는 "이 색상 효과는 프래그먼트 셰이더에서 구현해야겠네"라고 직관적으로 알 수 있게 돼.

3. 첫 번째 WebGL 프로그램 만들기 🚀

이론은 충분히 배웠으니 이제 직접 코드를 작성해볼 차례야! 가장 기본적인 WebGL 프로그램을 만들어보자. 빨간색 삼각형 하나를 화면에 그리는 간단한 예제로 시작할게.

HTML 구조 만들기

먼저 캔버스 요소를 HTML에 추가해야 해:

<canvas id="glCanvas" width="640" height="480">
  WebGL을 지원하지 않는 브라우저입니다.
</canvas>

JavaScript로 WebGL 초기화하기

이제 JavaScript로 WebGL 컨텍스트를 가져오고 초기화해보자:

// 캔버스 요소 가져오기
const canvas = document.getElementById('glCanvas');

// WebGL 컨텍스트 가져오기
const gl = canvas.getContext('webgl');

// WebGL이 지원되지 않는 경우 처리
if (!gl) {
  alert('WebGL을 지원하지 않는 브라우저입니다!');
  return;
}

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

여기까지 하면 검은색 캔버스가 준비된 거야. 이제 삼각형을 그려볼까?

셰이더 코드 작성하기 ✨

버텍스 셰이더와 프래그먼트 셰이더를 작성해보자:

// 버텍스 셰이더 소스
const vsSource = `
  attribute vec4 aVertexPosition;
  
  void main() {
    gl_Position = aVertexPosition;
  }
`;

// 프래그먼트 셰이더 소스
const fsSource = `
  precision mediump float;
  
  void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 빨간색
  }
`;

버텍스 셰이더는 정점의 위치를 그대로 사용하고, 프래그먼트 셰이더는 모든 픽셀을 빨간색으로 칠하도록 설정했어.

셰이더 컴파일 및 프로그램 생성

// 셰이더 컴파일 함수
function compileShader(gl, source, type) {
  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, vsSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(gl, fsSource, gl.FRAGMENT_SHADER);

// 셰이더 프로그램 생성
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);

if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
  console.error('셰이더 프로그램 링크 오류:', gl.getProgramInfoLog(shaderProgram));
  return;
}

gl.useProgram(shaderProgram);

삼각형 정점 데이터 설정하기

// 삼각형 정점 위치 데이터
const vertices = [
  -0.5, -0.5,  // 왼쪽 아래
   0.5, -0.5,  // 오른쪽 아래
   0.0,  0.5   // 위쪽 중앙
];

// 정점 버퍼 생성
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

// 셰이더의 attribute 변수 위치 가져오기
const vertexPosition = gl.getAttribLocation(shaderProgram, 'aVertexPosition');

// 버퍼에서 데이터를 어떻게 가져올지 설정
gl.vertexAttribPointer(
  vertexPosition,  // attribute 위치
  2,               // 각 정점당 값 개수 (x, y)
  gl.FLOAT,        // 데이터 타입
  false,           // 정규화 여부
  0,               // stride (0 = 자동 계산)
  0                // 버퍼 오프셋
);
gl.enableVertexAttribArray(vertexPosition);

삼각형 그리기

// 화면 지우기
gl.clear(gl.COLOR_BUFFER_BIT);

// 삼각형 그리기
gl.drawArrays(
  gl.TRIANGLES,    // 그리기 모드
  0,               // 시작 인덱스
  3                // 정점 개수
);

위 코드를 모두 합치면 아래와 같은 빨간색 삼각형이 화면에 나타날 거야! 🎉

WebGL 셰이더 프로그래밍: 첫 번째 삼각형 (0.0, 0.5) (-0.5, -0.5) (0.5, -0.5) (0, -1) (0, 1) (-1, 0) (1, 0)

축하해! 방금 너의 첫 번째 WebGL 프로그램을 만들었어. 이게 모든 WebGL 프로그램의 기본 구조야. 물론 실제 프로젝트에서는 더 복잡한 형태가 되겠지만, 기본 원리는 동일해. 이제 셰이더 언어에 대해 더 자세히 알아보자!

4. GLSL 셰이더 언어 기초 📝

셰이더를 작성하려면 GLSL(OpenGL Shading Language)이라는 언어를 알아야 해. C 언어와 비슷하지만, 벡터와 행렬 연산에 특화된 언어야. 그래픽 처리에 필요한 수학적 연산을 쉽게 할 수 있도록 설계되었지.

GLSL의 기본 데이터 타입 🧮

  1. float: 부동소수점 숫자 (예: 1.0, 3.14)
  2. int: 정수 (예: 1, 42)
  3. bool: 불리언 값 (true, false)
  4. vec2, vec3, vec4: 2D, 3D, 4D 벡터 (예: vec3(1.0, 0.5, 0.0))
  5. mat2, mat3, mat4: 2x2, 3x3, 4x4 행렬
  6. sampler2D: 2D 텍스처

변수 한정자 (Variable Qualifiers)

  1. attribute: 버텍스 셰이더에서만 사용, 각 정점마다 다른 값
  2. uniform: 모든 정점/픽셀에 동일하게 적용되는 값
  3. varying: 버텍스 셰이더에서 프래그먼트 셰이더로 값을 전달

내장 변수 (Built-in Variables)

  1. gl_Position: 버텍스 셰이더에서 정점의 최종 위치
  2. gl_FragColor: 프래그먼트 셰이더에서 픽셀의 최종 색상
  3. gl_PointSize: 점의 크기 (POINTS 모드에서 사용)

GLSL 코드 예제 💻

벡터 연산 예제:

// 벡터 선언
vec3 position = vec3(1.0, 2.0, 3.0);
vec3 color = vec3(1.0, 0.0, 0.0);  // 빨간색

// 벡터 연산
vec3 result = position + color;    // 벡터 덧셈
float length = length(position);   // 벡터 길이
vec3 normalized = normalize(position);  // 정규화된 벡터
float dotProduct = dot(position, color);  // 내적

조건문과 반복문:

// 조건문
if (color.r > 0.5) {
  color.g = 1.0;
} else {
  color.b = 1.0;
}

// 반복문
for (int i = 0; i < 3; i++) {
  color[i] *= 0.5;
}

함수 정의:

// 색상을 반전시키는 함수
vec3 invertColor(vec3 color) {
  return vec3(1.0) - color;
}

// 사용 예
vec3 originalColor = vec3(0.8, 0.3, 0.2);
vec3 inverted = invertColor(originalColor);

GLSL에는 그래픽 처리에 유용한 많은 내장 함수들이 있어:

유용한 GLSL 내장 함수들 🛠️

  1. 수학 함수: sin, cos, tan, pow, exp, log, sqrt, abs 등
  2. 벡터 함수: length, distance, dot, cross, normalize, reflect 등
  3. 텍스처 함수: texture2D, textureCube 등
  4. 보간 함수: mix, smoothstep, step 등
vec2(0.5, -0.3) vec2(-0.4, -0.5) A+B 벡터 덧셈 R G B mix() 색상 혼합 원본 변환됨 mat4 행렬 변환

GLSL은 처음에는 어려워 보일 수 있지만, 기본 개념만 이해하면 생각보다 쉽게 접근할 수 있어. 재능넷에서도 GLSL 관련 강의나 프로젝트를 찾아볼 수 있으니, 더 깊이 배우고 싶다면 참고해봐!

5. 버텍스 셰이더와 프래그먼트 셰이더 🧩

이제 WebGL의 두 핵심 셰이더에 대해 더 자세히 알아보자. 이 두 셰이더의 역할과 차이점을 이해하는 것이 셰이더 프로그래밍의 핵심이야!

버텍스 셰이더 (Vertex Shader) 📐

버텍스 셰이더는 3D 모델의 각 정점(vertex)마다 한 번씩 실행되는 프로그램이야. 주요 역할은:

  1. 정점의 위치 변환 (모델 → 월드 → 뷰 → 클립 공간)
  2. 정점 데이터(색상, 텍스처 좌표 등)를 프래그먼트 셰이더로 전달
  3. 정점 기반 계산 수행 (예: 조명 효과)

기본적인 버텍스 셰이더 예제:

// 버텍스 셰이더
attribute vec3 aPosition;        // 정점 위치
attribute vec2 aTexCoord;        // 텍스처 좌표
uniform mat4 uModelViewMatrix;   // 모델-뷰 행렬
uniform mat4 uProjectionMatrix;  // 투영 행렬
varying vec2 vTexCoord;          // 프래그먼트 셰이더로 전달할 텍스처 좌표

void main() {
  // 정점 위치 변환
  gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
  
  // 텍스처 좌표 전달
  vTexCoord = aTexCoord;
}

프래그먼트 셰이더 (Fragment Shader) 🎨

프래그먼트 셰이더는 화면에 그려질 각 픽셀마다 한 번씩 실행되는 프로그램이야. 주요 역할은:

  1. 픽셀의 최종 색상 결정
  2. 텍스처 샘플링 및 혼합
  3. 조명, 그림자, 특수 효과 계산

기본적인 프래그먼트 셰이더 예제:

// 프래그먼트 셰이더
precision mediump float;         // 부동소수점 정밀도 설정
varying vec2 vTexCoord;          // 버텍스 셰이더에서 전달받은 텍스처 좌표
uniform sampler2D uTexture;      // 텍스처
uniform vec3 uLightDirection;    // 조명 방향

void main() {
  // 텍스처에서 색상 샘플링
  vec4 texColor = texture2D(uTexture, vTexCoord);
  
  // 간단한 조명 효과 (예시)
  float light = max(dot(uLightDirection, vec3(0.0, 0.0, 1.0)), 0.3);
  
  // 최종 색상 결정
  gl_FragColor = texColor * vec4(vec3(light), 1.0);
}
버텍스 셰이더 vs 프래그먼트 셰이더 버텍스 셰이더 정점 단위 처리 프래그먼트 셰이더 픽셀 단위 처리

두 셰이더 간의 데이터 전달 📤📥

버텍스 셰이더에서 프래그먼트 셰이더로 데이터를 전달하려면 varying 변수를 사용해야 해. 이 과정에서 중요한 점은:

  1. 버텍스 셰이더에서 계산된 값은 정점 사이에서 보간되어 프래그먼트 셰이더로 전달돼.
  2. 각 셰이더에서 동일한 이름과 타입의 varying 변수를 선언해야 해.
  3. 버텍스 셰이더에서는 값을 설정하고, 프래그먼트 셰이더에서는 그 값을 읽어와.

예를 들어, 정점 색상을 프래그먼트 셰이더로 전달하는 코드:

// 버텍스 셰이더
attribute vec3 aPosition;
attribute vec3 aColor;    // 정점 색상
varying vec3 vColor;      // 프래그먼트 셰이더로 전달할 색상

void main() {
  gl_Position = vec4(aPosition, 1.0);
  vColor = aColor;        // 색상 값 설정
}

// 프래그먼트 셰이더
precision mediump float;
varying vec3 vColor;      // 버텍스 셰이더에서 전달받은 색상

void main() {
  gl_FragColor = vec4(vColor, 1.0);  // 전달받은 색상 사용
}

이렇게 두 셰이더가 협력하여 복잡한 그래픽 효과를 만들어내는 거야. 버텍스 셰이더는 형태를, 프래그먼트 셰이더는 색상과 질감을 담당한다고 생각하면 이해하기 쉬울 거야. 다음 섹션에서는 이 두 셰이더를 활용해 더 멋진 효과를 만드는 방법을 알아보자! 🎨

6. 텍스처와 라이팅 효과 구현하기 🌟

이제 기본 개념을 이해했으니, 좀 더 실용적인 기술을 배워보자! 텍스처 매핑과 라이팅은 3D 그래픽에서 가장 중요한 두 가지 요소야. 이 두 기술을 마스터하면 훨씬 더 사실적인 3D 장면을 만들 수 있어!

텍스처 매핑 (Texture Mapping) 🖼️

텍스처 매핑은 3D 모델의 표면에 이미지를 입히는 기술이야. 마치 종이 인형에 옷을 입히는 것처럼 생각하면 돼.

텍스처 로딩 및 설정 (JavaScript)

// 텍스처 로드 및 설정 함수
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);

    // 이미지 크기가 2의 제곱수인지 확인
    if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
      gl.generateMipmap(gl.TEXTURE_2D);
    } else {
      // 2의 제곱수가 아닌 경우 설정
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    }
  };
  image.src = url;

  return texture;
}

// 2의 제곱수 확인 함수
function isPowerOf2(value) {
  return (value & (value - 1)) === 0;
}

텍스처 좌표 설정

// 텍스처 좌표 데이터
const texCoords = [
  0.0, 0.0,  // 왼쪽 아래
  1.0, 0.0,  // 오른쪽 아래
  0.5, 1.0   // 위쪽 중앙
];

// 텍스처 좌표 버퍼 생성
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW);

// 셰이더의 attribute 변수 위치 가져오기
const texCoordLocation = gl.getAttribLocation(shaderProgram, 'aTexCoord');

// 버퍼에서 데이터를 어떻게 가져올지 설정
gl.vertexAttribPointer(
  texCoordLocation,  // attribute 위치
  2,                 // 각 텍스처 좌표당 값 개수 (s, t)
  gl.FLOAT,          // 데이터 타입
  false,             // 정규화 여부
  0,                 // stride
  0                  // 버퍼 오프셋
);
gl.enableVertexAttribArray(texCoordLocation);

텍스처 셰이더 코드

// 버텍스 셰이더
attribute vec4 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;

void main() {
  gl_Position = aPosition;
  vTexCoord = aTexCoord;
}

// 프래그먼트 셰이더
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;

void main() {
  gl_FragColor = texture2D(uTexture, vTexCoord);
}
텍스처 매핑 과정 텍스처 이미지 (0,0) (1,0) (1,1) (0,1) (0.2, 0.2) (0.8, 0.2) (0.5, 0.8) 텍스처 좌표 (UV) 텍스처가 적용된 3D 모델

라이팅 효과 (Lighting Effects) 💡

라이팅은 3D 장면에 깊이감과 사실감을 더해주는 핵심 요소야. 기본적인 라이팅 모델에는 세 가지 구성 요소가 있어:

  1. 주변광(Ambient): 모든 방향에서 균일하게 오는 기본 조명
  2. 확산광(Diffuse): 물체의 표면에 직접 닿는 조명
  3. 반사광(Specular): 물체 표면에서 반사되는 하이라이트

라이팅 셰이더 코드

// 버텍스 셰이더
attribute vec4 aPosition;
attribute vec3 aNormal;       // 법선 벡터
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat4 uNormalMatrix;   // 법선 변환 행렬
varying vec3 vNormal;         // 프래그먼트 셰이더로 전달할 법선
varying vec3 vPosition;       // 프래그먼트 셰이더로 전달할 위치

void main() {
  // 정점 위치 변환
  vec4 position = uModelViewMatrix * aPosition;
  gl_Position = uProjectionMatrix * position;
  
  // 법선 벡터 변환
  vNormal = vec3(uNormalMatrix * vec4(aNormal, 0.0));
  
  // 정점 위치 전달
  vPosition = vec3(position);
}

// 프래그먼트 셰이더
precision mediump float;
varying vec3 vNormal;
varying vec3 vPosition;
uniform vec3 uLightPosition;   // 조명 위치
uniform vec3 uLightColor;      // 조명 색상
uniform vec3 uAmbientColor;    // 주변광 색상
uniform vec3 uDiffuseColor;    // 물체 색상
uniform float uShininess;      // 광택도
uniform vec3 uSpecularColor;   // 반사광 색상

void main() {
  // 정규화된 법선
  vec3 normal = normalize(vNormal);
  
  // 조명 방향
  vec3 lightDirection = normalize(uLightPosition - vPosition);
  
  // 주변광 계산
  vec3 ambient = uAmbientColor;
  
  // 확산광 계산
  float diffuseFactor = max(dot(normal, lightDirection), 0.0);
  vec3 diffuse = diffuseFactor * uLightColor * uDiffuseColor;
  
  // 반사광 계산
  vec3 viewDirection = normalize(-vPosition);
  vec3 reflectDirection = reflect(-lightDirection, normal);
  float specularFactor = pow(max(dot(viewDirection, reflectDirection), 0.0), uShininess);
  vec3 specular = specularFactor * uSpecularColor * uLightColor;
  
  // 최종 색상 계산
  vec3 result = ambient + diffuse + specular;
  gl_FragColor = vec4(result, 1.0);
}
라이팅 효과의 구성 요소 주변광 (Ambient) 확산광 (Diffuse) 반사광 (Specular)

텍스처와 라이팅을 결합하면 정말 멋진 3D 장면을 만들 수 있어! 이 두 기술은 WebGL 셰이더 프로그래밍의 기본 중의 기본이니 꼭 마스터하도록 해. 재능넷에서도 이런 기술을 활용한 3D 웹 프로젝트가 인기가 많다고 하더라고. 다음 섹션에서는 이런 기본 효과에 인터랙티브 요소를 추가하는 방법을 알아볼 거야! 🎮

7. 인터랙티브 효과와 애니메이션 🎬

정적인 3D 모델도 멋지지만, 사용자와 상호작용하거나 움직이는 그래픽은 훨씬 더 매력적이지! 이번에는 WebGL 셰이더를 활용해 인터랙티브 효과와 애니메이션을 구현하는 방법을 알아보자.

시간 기반 애니메이션 ⏱️

셰이더에서 시간 값을 활용하면 다양한 애니메이션 효과를 만들 수 있어. JavaScript에서 시간 값을 셰이더로 전달하는 방법을 살펴보자:

// JavaScript에서 시간 값 전달
let then = 0;

function render(now) {
  // 시간을 초 단위로 변환
  now *= 0.001;  // 밀리초를 초로 변환
  const deltaTime = now - then;
  then = now;
  
  // 셰이더에 시간 값 전달
  const timeLocation = gl.getUniformLocation(shaderProgram, 'uTime');
  gl.uniform1f(timeLocation, now);
  
  // 렌더링 코드...
  
  // 다음 프레임 요청
  requestAnimationFrame(render);
}

// 애니메이션 시작
requestAnimationFrame(render);

셰이더에서 시간 값을 활용한 파동 효과 예제:

// 버텍스 셰이더
attribute vec4 aPosition;
attribute vec2 aTexCoord;
uniform float uTime;      // 시간 값
varying vec2 vTexCoord;

void main() {
  // 시간에 따라 정점 위치 변경 (파동 효과)
  float wave = sin(aPosition.x * 10.0 + uTime) * 0.1;
  vec4 position = aPosition;
  position.y += wave;
  
  gl_Position = position;
  vTexCoord = aTexCoord;
}

// 프래그먼트 셰이더
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
uniform float uTime;      // 시간 값

void main() {
  // 시간에 따라 색상 변화
  vec2 texCoord = vTexCoord;
  texCoord.x += sin(texCoord.y * 10.0 + uTime) * 0.05;
  
  gl_FragColor = texture2D(uTexture, texCoord);
}

사용자 인터랙션 처리 🖱️

마우스나 터치 이벤트를 셰이더에 전달하여 인터랙티브 효과를 만들 수 있어:

// 마우스 위치 추적
let mouseX = 0.5;
let mouseY = 0.5;

canvas.addEventListener('mousemove', (event) => {
  // 캔버스 내에서의 상대적 위치 (0.0 ~ 1.0)
  const rect = canvas.getBoundingClientRect();
  mouseX = (event.clientX - rect.left) / canvas.width;
  mouseY = 1.0 - (event.clientY - rect.top) / canvas.height;  // Y축 반전
  
  // 셰이더에 마우스 위치 전달
  const mouseLocation = gl.getUniformLocation(shaderProgram, 'uMouse');
  gl.uniform2f(mouseLocation, mouseX, mouseY);
});

마우스 위치를 활용한 인터랙티브 셰이더 예제:

// 프래그먼트 셰이더
precision mediump float;
varying vec2 vTexCoord;
uniform vec2 uMouse;      // 마우스 위치 (0.0 ~ 1.0)
uniform sampler2D uTexture;

void main() {
  // 마우스와의 거리 계산
  float distance = length(vTexCoord - uMouse);
  
  // 마우스 주변에 원형 하이라이트 효과
  float circle = smoothstep(0.2, 0.21, distance);
  
  // 텍스처 색상 가져오기
  vec4 texColor = texture2D(uTexture, vTexCoord);
  
  // 마우스 주변 색상 변경
  vec4 highlightColor = vec4(1.0, 0.5, 0.0, 1.0);  // 주황색
  vec4 finalColor = mix(highlightColor, texColor, circle);
  
  gl_FragColor = finalColor;
}
인터랙티브 셰이더 효과 시간 기반 애니메이션 마우스 인터랙션 JavaScript 이벤트 처리 Uniform 변수 시간, 마우스 위치 셰이더 시각적 효과 계산

고급 애니메이션 기법 🚀

셰이더를 활용한 더 복잡한 애니메이션 효과를 만들어보자. 노이즈 함수를 활용한 유기적인 움직임 예제:

// 프래그먼트 셰이더
precision mediump float;
varying vec2 vTexCoord;
uniform float uTime;

// 2D 노이즈 함수 (간단한 구현)
float noise(vec2 st) {
  return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}

// 부드러운 노이즈를 위한 보간 함수
float smoothNoise(vec2 st) {
  vec2 ipos = floor(st);
  vec2 fpos = fract(st);
  
  // 4개의 코너 값
  float a = noise(ipos);
  float b = noise(ipos + vec2(1.0, 0.0));
  float c = noise(ipos + vec2(0.0, 1.0));
  float d = noise(ipos + vec2(1.0, 1.0));
  
  // 부드러운 보간
  vec2 u = smoothstep(0.0, 1.0, fpos);
  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

void main() {
  // 시간에 따라 움직이는 노이즈 패턴
  vec2 pos = vTexCoord * 10.0;
  pos.x += uTime * 0.5;
  
  // 여러 레이어의 노이즈 합성
  float n = 0.0;
  n += smoothNoise(pos) * 0.5;
  n += smoothNoise(pos * 2.0) * 0.25;
  n += smoothNoise(pos * 4.0) * 0.125;
  n += smoothNoise(pos * 8.0) * 0.0625;
  
  // 노이즈 값에 따라 색상 결정
  vec3 color1 = vec3(0.2, 0.5, 0.9);  // 파란색
  vec3 color2 = vec3(0.9, 0.4, 0.1);  // 주황색
  vec3 finalColor = mix(color1, color2, n);
  
  gl_FragColor = vec4(finalColor, 1.0);
}

이 코드는 여러 레이어의 노이즈를 합성하여 구름이나 불, 물과 같은 자연스러운 움직임을 표현할 수 있어. 이런 기법을 프랙탈 노이즈 또는 Perlin 노이즈라고 불러.

인터랙티브 효과와 애니메이션은 웹 경험을 훨씬 더 매력적으로 만들어줘. 사용자의 참여를 유도하고 시각적 흥미를 더하는 데 큰 역할을 하지. 이런 기술을 마스터하면 포트폴리오 웹사이트, 인터랙티브 데이터 시각화, 웹 게임 등 다양한 프로젝트에 활용할 수 있을 거야! 🎮

8. 성능 최적화 테크닉 ⚡

WebGL은 강력하지만, 잘못 사용하면 성능 문제가 발생할 수 있어. 특히 모바일 기기에서는 더욱 그렇지. 이번에는 WebGL 셰이더 프로그램의 성능을 최적화하는 방법을 알아보자!

셰이더 코드 최적화 📊

셰이더는 GPU에서 수많은 픽셀에 대해 병렬로 실행되기 때문에, 작은 최적화도 큰 성능 향상을 가져올 수 있어. 몇 가지 핵심 최적화 방법을 살펴보자:

  1. 계산 비용이 높은 함수 피하기

    sin, cos, pow, exp 같은 함수는 계산 비용이 높아. 가능하면 미리 계산하거나 근사값을 사용해:

    // 비효율적인 코드
    float result = pow(x, 5.0);
    
    // 더 효율적인 코드
    float result = x * x * x * x * x;
  2. 조건문 최소화하기

    GPU는 병렬 처리에 최적화되어 있어서 조건문(if-else)이 성능을 저하시킬 수 있어. 가능하면 수학적 표현으로 대체해:

    // 비효율적인 코드
    if (x < 0.5) {
      color = vec3(1.0, 0.0, 0.0);
    } else {
      color = vec3(0.0, 0.0, 1.0);
    }
    
    // 더 효율적인 코드
    float t = step(0.5, x);
    color = mix(vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0), t);
  3. 적절한 정밀도 사용하기

    모든 변수에 highp를 사용할 필요는 없어. 필요에 따라 적절한 정밀도를 선택해:

    precision mediump float;  // 대부분의 경우 충분
    
    // 정밀도가 중요한 계산에만 highp 사용
    highp float detailedCalculation = /* ... */;
  4. 루프 최적화하기

    루프는 가능한 한 간단하게 유지하고, 상수 반복 횟수를 사용해:

    // 비효율적인 코드 (동적 루프 범위)
    for (int i = 0; i < someVaryingValue; i++) {
      // ...
    }
    
    // 더 효율적인 코드 (상수 루프 범위)
    const int LOOP_COUNT = 10;
    for (int i = 0; i < LOOP_COUNT; i++) {
      // ...
    }

버퍼 및 텍스처 최적화 💾

JavaScript 측에서도 최적화할 수 있는 부분이 많아:

  1. 버퍼 재사용하기

    매 프레임마다 새 버퍼를 생성하지 말고, 기존 버퍼를 업데이트해:

    // 버퍼 한 번만 생성
    const positionBuffer = gl.createBuffer();
    
    // 렌더링 루프에서 데이터만 업데이트
    function render() {
      gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(updatedPositions), gl.DYNAMIC_DRAW);
      // ...
    }
  2. 적절한 텍스처 크기 사용하기

    너무 큰 텍스처는 메모리를 낭비하고 성능을 저하시켜. 2의 제곱 크기를 사용하는 것이 좋아:

    // 좋은 텍스처 크기 예시
    const textureSizes = [
      128, 256, 512, 1024, 2048  // 2의 제곱수
    ];
  3. 미리 계산된 값 사용하기

    셰이더에서 매번 계산하지 말고, 가능하면 JavaScript에서 미리 계산해:

    // JavaScript에서 행렬 계산
    const modelViewMatrix = mat4.create();
    mat4.multiply(modelViewMatrix, viewMatrix, modelMatrix);
    
    // 계산된 행렬을 셰이더에 전달
    gl.uniformMatrix4fv(
      gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
      false,
      modelViewMatrix
    );
WebGL 성능 최적화 저성능 중간성능 고성능 셰이더 코드 최적화 • 계산 비용 줄이기 • 조건문 최소화 버퍼 및 텍스처 최적화 • 버퍼 재사용 • 적절한 텍스처 크기 60 FPS

렌더링 최적화 🖼️

전체 렌더링 파이프라인을 최적화하는 방법도 중요해:

  1. 필요할 때만 렌더링하기

    변화가 없을 때는 불필요한 렌더링을 피해:

    let needsRender = true;
    
    function render() {
      if (needsRender) {
        // 렌더링 코드...
        needsRender = false;
      }
      requestAnimationFrame(render);
    }
    
    // 변화가 있을 때만 렌더링 플래그 설정
    function onChangeDetected() {
      needsRender = true;
    }
  2. 복잡도 수준 조절하기

    기기 성능에 따라 셰이더 복잡도를 조절해:

    // 기기 성능 감지
    function detectPerformance() {
      const start = performance.now();
      // 간단한 벤치마크 테스트
      for (let i = 0; i < 10000; i++) {
        Math.sqrt(i);
      }
      const end = performance.now();
      return end - start;
    }
    
    // 성능에 따라 셰이더 선택
    const perfScore = detectPerformance();
    let shaderSource;
    
    if (perfScore < 5) {
      shaderSource = highQualityShader;
    } else if (perfScore < 20) {
      shaderSource = mediumQualityShader;
    } else {
      shaderSource = lowQualityShader;
    }

성능 최적화는 사용자 경험에 직접적인 영향을 미치는 중요한 부분이야. 특히 모바일 기기에서는 배터리 소모와 발열 문제도 고려해야 하지. 재능넷에서 WebGL 프로젝트를 의뢰하거나 수행할 때 이런 최적화 기법을 적용하면 훨씬 더 만족스러운 결과물을 얻을 수 있을 거야! 🚀

9. 실전 프로젝트: 인터랙티브 3D 모델 뷰어 🔍

지금까지 배운 내용을 종합해서 실전 프로젝트를 만들어보자! 간단한 3D 모델 뷰어를 구현해볼 거야. 이 뷰어는 마우스로 모델을 회전시키고, 줌인/줌아웃할 수 있는 기능을 갖추고 있어.

프로젝트 개요 📋

이 프로젝트에서 구현할 기능들은 다음과 같아:

  1. 3D 모델 로딩 및 표시
  2. 마우스 드래그로 모델 회전
  3. 마우스 휠로 줌인/줌아웃
  4. 텍스처 매핑
  5. 기본 조명 효과

이 프로젝트는 포트폴리오 웹사이트나 제품 소개 페이지에 활용하기 좋은 예제야. 재능넷에서도 이런 종류의 인터랙티브 3D 뷰어를 의뢰하는 경우가 많다고 해!

자바스크립트 WebGL 셰이더 프로그래밍 웹에서 3D 그래픽의 마법을 부리는 방법 🚀 드래그: 회전 휠: 줌 더블클릭: 리셋

HTML 구조

<canvas id="modelViewer" width="800" height="600">
  WebGL을 지원하지 않는 브라우저입니다.
</canvas>

<div class="controls">
  <button id="resetBtn">뷰 리셋</button>
  <select id="textureSelect">
    <option value="texture1">텍스처 1</option>
    <option value="texture2">텍스처 2</option>
  </select>
</div>

셰이더 코드

버텍스 셰이더:

// 버텍스 셰이더
attribute vec4 aPosition;
attribute vec3 aNormal;
attribute vec2 aTexCoord;

uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat4 uNormalMatrix;

varying vec3 vNormal;
varying vec3 vPosition;
varying vec2 vTexCoord;

void main() {
  // 정점 위치 변환
  vec4 position = uModelViewMatrix * aPosition;
  gl_Position = uProjectionMatrix * position;
  
  // 법선 벡터 변환
  vNormal = vec3(uNormalMatrix * vec4(aNormal, 0.0));
  
  // 정점 위치 및 텍스처 좌표 전달
  vPosition = vec3(position);
  vTexCoord = aTexCoord;
}

프래그먼트 셰이더:

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

varying vec3 vNormal;
varying vec3 vPosition;
varying vec2 vTexCoord;

uniform vec3 uLightPosition;
uniform vec3 uLightColor;
uniform vec3 uAmbientLight;
uniform sampler2D uTexture;

void main() {
  // 정규화된 법선
  vec3 normal = normalize(vNormal);
  
  // 조명 방향
  vec3 lightDirection = normalize(uLightPosition - vPosition);
  
  // 주변광 계산
  vec3 ambient = uAmbientLight;
  
  // 확산광 계산
  float diffuseFactor = max(dot(normal, lightDirection), 0.0);
  vec3 diffuse = diffuseFactor * uLightColor;
  
  // 텍스처 색상 가져오기
  vec4 texColor = texture2D(uTexture, vTexCoord);
  
  // 최종 색상 계산
  vec3 result = (ambient + diffuse) * texColor.rgb;
  gl_FragColor = vec4(result, texColor.a);
}

JavaScript 구현

주요 초기화 및 렌더링 코드:

// 주요 변수 선언
let gl;
let shaderProgram;
let modelBuffers;
let texture;
let rotation = [0, 0, 0];
let zoom = 5.0;
let isDragging = false;
let lastMouseX, lastMouseY;

// WebGL 초기화
function initWebGL() {
  const canvas = document.getElementById('modelViewer');
  gl = canvas.getContext('webgl');
  
  if (!gl) {
    alert('WebGL을 지원하지 않는 브라우저입니다!');
    return;
  }
  
  // 셰이더 초기화
  const vertexShader = compileShader(gl, vsSource, gl.VERTEX_SHADER);
  const fragmentShader = compileShader(gl, fsSource, gl.FRAGMENT_SHADER);
  
  // 셰이더 프로그램 생성
  shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  
  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    alert('셰이더 프로그램 초기화 실패!');
    return;
  }
  
  gl.useProgram(shaderProgram);
  
  // 모델 로드 및 버퍼 초기화
  loadModel();
  
  // 텍스처 로드
  loadTexture('model_texture.jpg');
  
  // 이벤트 리스너 설정
  setupEventListeners();
  
  // 렌더링 시작
  render();
}

// 모델 데이터 로드 및 버퍼 설정
function loadModel() {
  // 여기서는 간단한 큐브 모델을 사용
  // 실제로는 OBJ, glTF 등의 3D 모델 포맷을 파싱하는 코드가 들어갈 수 있음
  
  // 정점 위치 데이터
  const positions = [
    // 앞면
    -1.0, -1.0,  1.0,
     1.0, -1.0,  1.0,
     1.0,  1.0,  1.0,
    -1.0,  1.0,  1.0,
    // 뒷면
    -1.0, -1.0, -1.0,
    -1.0,  1.0, -1.0,
     1.0,  1.0, -1.0,
     1.0, -1.0, -1.0,
    // 윗면
    -1.0,  1.0, -1.0,
    -1.0,  1.0,  1.0,
     1.0,  1.0,  1.0,
     1.0,  1.0, -1.0,
    // 아랫면
    -1.0, -1.0, -1.0,
     1.0, -1.0, -1.0,
     1.0, -1.0,  1.0,
    -1.0, -1.0,  1.0,
    // 오른쪽면
     1.0, -1.0, -1.0,
     1.0,  1.0, -1.0,
     1.0,  1.0,  1.0,
     1.0, -1.0,  1.0,
    // 왼쪽면
    -1.0, -1.0, -1.0,
    -1.0, -1.0,  1.0,
    -1.0,  1.0,  1.0,
    -1.0,  1.0, -1.0
  ];
  
  // 법선 데이터
  const normals = [
    // 앞면
    0.0,  0.0,  1.0,
    0.0,  0.0,  1.0,
    0.0,  0.0,  1.0,
    0.0,  0.0,  1.0,
    // 뒷면
    0.0,  0.0, -1.0,
    0.0,  0.0, -1.0,
    0.0,  0.0, -1.0,
    0.0,  0.0, -1.0,
    // 윗면
    0.0,  1.0,  0.0,
    0.0,  1.0,  0.0,
    0.0,  1.0,  0.0,
    0.0,  1.0,  0.0,
    // 아랫면
    0.0, -1.0,  0.0,
    0.0, -1.0,  0.0,
    0.0, -1.0,  0.0,
    0.0, -1.0,  0.0,
    // 오른쪽면
    1.0,  0.0,  0.0,
    1.0,  0.0,  0.0,
    1.0,  0.0,  0.0,
    1.0,  0.0,  0.0,
    // 왼쪽면
   -1.0,  0.0,  0.0,
   -1.0,  0.0,  0.0,
   -1.0,  0.0,  0.0,
   -1.0,  0.0,  0.0
  ];
  
  // 텍스처 좌표
  const textureCoords = [
    // 앞면
    0.0, 0.0,
    1.0, 0.0,
    1.0, 1.0,
    0.0, 1.0,
    // 뒷면
    1.0, 0.0,
    1.0, 1.0,
    0.0, 1.0,
    0.0, 0.0,
    // 윗면
    0.0, 1.0,
    0.0, 0.0,
    1.0, 0.0,
    1.0, 1.0,
    // 아랫면
    1.0, 1.0,
    0.0, 1.0,
    0.0, 0.0,
    1.0, 0.0,
    // 오른쪽면
    1.0, 0.0,
    1.0, 1.0,
    0.0, 1.0,
    0.0, 0.0,
    // 왼쪽면
    0.0, 0.0,
    1.0, 0.0,
    1.0, 1.0,
    0.0, 1.0
  ];
  
  // 인덱스 데이터
  const indices = [
    0,  1,  2,    0,  2,  3,  // 앞면
    4,  5,  6,    4,  6,  7,  // 뒷면
    8,  9,  10,   8,  10, 11, // 윗면
    12, 13, 14,   12, 14, 15, // 아랫면
    16, 17, 18,   16, 18, 19, // 오른쪽면
    20, 21, 22,   20, 22, 23  // 왼쪽면
  ];
  
  // 버퍼 생성 및 데이터 설정
  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
  
  const normalBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
  
  const textureCoordBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
  
  const indexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
  
  // 버퍼 정보 저장
  modelBuffers = {
    position: positionBuffer,
    normal: normalBuffer,
    textureCoord: textureCoordBuffer,
    indices: indexBuffer,
    vertexCount: indices.length
  };
}

// 이벤트 리스너 설정
function setupEventListeners() {
  const canvas = document.getElementById('modelViewer');
  
  // 마우스 드래그로 회전
  canvas.addEventListener('mousedown', (e) => {
    isDragging = true;
    lastMouseX = e.clientX;
    lastMouseY = e.clientY;
  });
  
  canvas.addEventListener('mouseup', () => {
    isDragging = false;
  });
  
  canvas.addEventListener('mousemove', (e) => {
    if (!isDragging) return;
    
    const deltaX = e.clientX - lastMouseX;
    const deltaY = e.clientY - lastMouseY;
    
    rotation[1] += deltaX * 0.01;
    rotation[0] += deltaY * 0.01;
    
    lastMouseX = e.clientX;
    lastMouseY = e.clientY;
  });
  
  // 마우스 휠로 줌
  canvas.addEventListener('wheel', (e) => {
    e.preventDefault();
    zoom += e.deltaY * 0.01;
    zoom = Math.max(2.0, Math.min(zoom, 10.0));  // 줌 범위 제한
  });
  
  // 뷰 리셋 버튼
  document.getElementById('resetBtn').addEventListener('click', () => {
    rotation = [0, 0, 0];
    zoom = 5.0;
  });
  
  // 텍스처 변경
  document.getElementById('textureSelect').addEventListener('change', (e) => {
    loadTexture(e.target.value + '.jpg');
  });
}

// 렌더링 함수
function render() {
  // 화면 지우기
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clearDepth(1.0);
  gl.enable(gl.DEPTH_TEST);
  gl.depthFunc(gl.LEQUAL);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  
  // 투영 행렬 설정
  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  const projectionMatrix = mat4.create();
  mat4.perspective(projectionMatrix, 45 * Math.PI / 180, aspect, 0.1, 100.0);
  
  // 모델-뷰 행렬 설정
  const modelViewMatrix = mat4.create();
  mat4.translate(modelViewMatrix, modelViewMatrix, [0.0, 0.0, -zoom]);
  mat4.rotate(modelViewMatrix, modelViewMatrix, rotation[0], [1, 0, 0]);
  mat4.rotate(modelViewMatrix, modelViewMatrix, rotation[1], [0, 1, 0]);
  
  // 법선 행렬 설정
  const normalMatrix = mat4.create();
  mat4.invert(normalMatrix, modelViewMatrix);
  mat4.transpose(normalMatrix, normalMatrix);
  
  // 셰이더 변수 설정
  const projectionMatrixLocation = gl.getUniformLocation(shaderProgram, 'uProjectionMatrix');
  const modelViewMatrixLocation = gl.getUniformLocation(shaderProgram, 'uModelViewMatrix');
  const normalMatrixLocation = gl.getUniformLocation(shaderProgram, 'uNormalMatrix');
  
  gl.uniformMatrix4fv(projectionMatrixLocation, false, projectionMatrix);
  gl.uniformMatrix4fv(modelViewMatrixLocation, false, modelViewMatrix);
  gl.uniformMatrix4fv(normalMatrixLocation, false, normalMatrix);
  
  // 조명 설정
  const lightPositionLocation = gl.getUniformLocation(shaderProgram, 'uLightPosition');
  const lightColorLocation = gl.getUniformLocation(shaderProgram, 'uLightColor');
  const ambientLightLocation = gl.getUniformLocation(shaderProgram, 'uAmbientLight');
  
  gl.uniform3fv(lightPositionLocation, [5.0, 5.0, 5.0]);
  gl.uniform3fv(lightColorLocation, [1.0, 1.0, 1.0]);
  gl.uniform3fv(ambientLightLocation, [0.2, 0.2, 0.2]);
  
  // 텍스처 설정
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.uniform1i(gl.getUniformLocation(shaderProgram, 'uTexture'), 0);
  
  // 버퍼 바인딩 및 attribute 설정
  const positionLocation = gl.getAttribLocation(shaderProgram, 'aPosition');
  gl.bindBuffer(gl.ARRAY_BUFFER, modelBuffers.position);
  gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(positionLocation);
  
  const normalLocation = gl.getAttribLocation(shaderProgram, 'aNormal');
  gl.bindBuffer(gl.ARRAY_BUFFER, modelBuffers.normal);
  gl.vertexAttribPointer(normalLocation, 3, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(normalLocation);
  
  const texCoordLocation = gl.getAttribLocation(shaderProgram, 'aTexCoord');
  gl.bindBuffer(gl.ARRAY_BUFFER, modelBuffers.textureCoord);
  gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(texCoordLocation);
  
  // 인덱스 버퍼 바인딩
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, modelBuffers.indices);
  
  // 모델 그리기
  gl.drawElements(gl.TRIANGLES, modelBuffers.vertexCount, gl.UNSIGNED_SHORT, 0);
  
  // 다음 프레임 요청
  requestAnimationFrame(render);
}

이 프로젝트는 WebGL 셰이더 프로그래밍의 여러 측면을 종합적으로 활용한 좋은 예시야. 실제로 이런 3D 뷰어는 온라인 쇼핑몰, 교육 사이트, 포트폴리오 등 다양한 웹사이트에서 활용되고 있어. 이 코드를 기반으로 더 복잡한 모델 로딩, 다양한 셰이더 효과, 추가 인터랙션 등을 구현해볼 수 있을 거야! 🚀

마무리: 셰이더의 무한한 가능성 🌈

지금까지 자바스크립트 WebGL 셰이더 프로그래밍의 기본부터 고급 기술까지 함께 알아봤어. 이제 너도 웹 브라우저에서 멋진 3D 그래픽을 만들 수 있는 기초 지식을 갖추게 됐어!

셰이더 프로그래밍은 기술적 지식과 창의성이 만나는 독특한 분야야. 수학과 프로그래밍의 논리적 사고방식이 필요하면서도, 동시에 예술적 감각과 창의력을 발휘할 수 있지. 그래서 많은 개발자들이 셰이더 프로그래밍에 매료되는 거야.

이 글에서 배운 내용을 바탕으로 자신만의 셰이더를 만들어보고, 다양한 효과를 실험해보길 권해. 처음에는 어려울 수 있지만, 조금씩 경험을 쌓다 보면 놀라운 결과물을 만들 수 있을 거야!

재능넷에서도 WebGL과 셰이더 프로그래밍 관련 프로젝트가 많이 진행되고 있어. 자신의 셰이더 프로그래밍 기술을 활용해 새로운 기회를 찾아보는 것도 좋은 방법이야. 🚀

마지막으로, 셰이더 프로그래밍은 끊임없이 발전하는 분야야. 새로운 기술과 트렌드를 계속 학습하고, 커뮤니티와 함께 성장해 나가길 바랄게. 웹의 시각적 한계를 뛰어넘는 멋진 경험을 만들어내는 셰이더 아티스트가 되길 응원해! 🎨✨

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.