Skip to main content
민트라

TSL 입문 - GLSL 없이 Three.js 쉐이더 작성하기

"쉐이더 배우고 싶은데 GLSL이 너무 어려워요."

Three.js 커뮤니티에서 가장 많이 듣는 말입니다. GLSL은 C 스타일 문법에 GPU 특유의 개념까지 더해져서 JavaScript 개발자에게는 진입장벽이 높습니다. 하지만 이제 TSL이 있습니다.

TSL이란? #

TSL(Three.js Shading Language)은 Three.js r160부터 도입된 새로운 쉐이더 작성 방식입니다. JavaScript 함수로 쉐이더를 작성하면, Three.js가 WebGL용 GLSL 또는 WebGPU용 WGSL로 자동 변환해줍니다.

// 기존 GLSL 방식
const fragmentShader = `
  varying vec2 vUv;
  uniform float uTime;

  void main() {
    vec3 color = vec3(vUv.x, vUv.y, sin(uTime));
    gl_FragColor = vec4(color, 1.0);
  }
`;

// TSL 방식
import { color, sin, time, uv } from 'three/tsl';

const colorNode = color(uv().x, uv().y, sin(time));
material.colorNode = colorNode;

같은 결과인데 JavaScript 문법으로 작성합니다. 타입 에러도 IDE가 잡아주고, 자동완성도 됩니다.

환경 설정 #

TSL을 사용하려면 Three.js WebGPU 버전을 import해야 합니다.

// 기존 방식
import * as THREE from 'three';

// TSL 사용을 위한 방식
import * as THREE from 'three/webgpu';
import { color, uv, sin, time, uniform } from 'three/tsl';

WebGPU를 지원하지 않는 브라우저에서는 자동으로 WebGL로 폴백됩니다. 걱정하지 않아도 됩니다.

첫 번째 TSL 쉐이더: 그라데이션 #

가장 기본적인 UV 그라데이션부터 시작합니다.

import * as THREE from 'three/webgpu';
import { color, uv } from 'three/tsl';

// 씬 설정
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight);
camera.position.z = 2;

// TSL 머티리얼
const material = new THREE.MeshBasicNodeMaterial();
material.colorNode = color(uv().x, uv().y, 0.5);

const geometry = new THREE.PlaneGeometry(2, 2);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 렌더러 (WebGPU 자동 사용)
const renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

function animate() {
  renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);

uv()는 0~1 사이의 텍스처 좌표를 반환합니다. uv().x는 가로 위치, uv().y는 세로 위치입니다. 이걸 RGB 색상에 대입하면 그라데이션이 됩니다.

시간 기반 애니메이션 #

움직이는 쉐이더를 만들어봅시다.

import { color, uv, sin, time } from 'three/tsl';

// 시간에 따라 색상이 변하는 쉐이더
const r = sin(time.mul(2)).mul(0.5).add(0.5);  // 0~1 사이 반복
const g = sin(time.mul(3)).mul(0.5).add(0.5);
const b = sin(time.add(uv().x.mul(10))).mul(0.5).add(0.5);

material.colorNode = color(r, g, b);

TSL에서는 연산자 대신 메서드를 사용합니다.

// JavaScript 연산자 vs TSL 메서드
a + b  →  a.add(b)
a - b  →  a.sub(b)
a * b  →  a.mul(b)
a / b  →  a.div(b)

처음엔 어색하지만, 체이닝이 가능해서 복잡한 수식도 깔끔하게 작성할 수 있습니다.

// sin(time * 2) * 0.5 + 0.5
sin(time.mul(2)).mul(0.5).add(0.5)

Uniform 사용하기 #

외부에서 쉐이더 값을 제어하고 싶을 때 uniform을 사용합니다.

import { color, uv, uniform } from 'three/tsl';

// uniform 생성
const uColor1 = uniform(new THREE.Color('#ff0000'));
const uColor2 = uniform(new THREE.Color('#0000ff'));
const uMix = uniform(0);

// 두 색상 보간
material.colorNode = uColor1.mix(uColor2, uMix);

// 나중에 값 변경
uMix.value = 0.5;  // 중간색
uColor1.value.set('#00ff00');  // 색상 변경

uniform()으로 생성한 값은 .value로 언제든 변경할 수 있습니다. GUI 라이브러리와 연동하기 좋습니다.

import GUI from 'lil-gui';

const gui = new GUI();
gui.add(uMix, 'value', 0, 1).name('Mix');
gui.addColor({ color: '#ff0000' }, 'color').onChange(v => {
  uColor1.value.set(v);
});

노이즈 패턴 만들기 #

TSL에는 노이즈 함수가 내장되어 있습니다.

import { color, uv, mx_noise_float } from 'three/tsl';

// Perlin 노이즈
const noise = mx_noise_float(uv().mul(5));

// 노이즈를 흑백으로 표시
material.colorNode = color(noise, noise, noise);

mx_noise_float는 MaterialX 기반 노이즈입니다. 스케일을 조절하려면 UV에 곱하면 됩니다.

움직이는 노이즈:

import { color, uv, time, vec3, mx_noise_float } from 'three/tsl';

const noiseInput = vec3(
  uv().x.mul(5),
  uv().y.mul(5),
  time.mul(0.5)
);

const noise = mx_noise_float(noiseInput);
material.colorNode = color(noise, noise, noise);

3차원 노이즈의 z축에 시간을 넣으면 자연스럽게 흐르는 효과가 납니다.

프래그먼트 vs 버텍스 #

지금까지는 색상(프래그먼트)만 다뤘습니다. 버텍스도 TSL로 제어할 수 있습니다.

import { positionLocal, sin, time } from 'three/tsl';

// 물결치는 평면
const position = positionLocal.clone();
position.y = position.y.add(
  sin(position.x.mul(5).add(time.mul(2))).mul(0.1)
);

material.positionNode = position;

positionLocal은 각 버텍스의 로컬 좌표입니다. y값에 sin 파동을 더하면 물결 효과가 됩니다.

주의: 버텍스 쉐이더에서 위치를 변경하면 노멀도 다시 계산해야 자연스러운 라이팅이 됩니다.

import { normalLocal, positionLocal, sin, time, normalize, cross, vec3 } from 'three/tsl';

// 위치 변형
const displaced = positionLocal.clone();
displaced.y = displaced.y.add(sin(displaced.x.mul(5).add(time)).mul(0.1));

material.positionNode = displaced;

// 노멀 재계산 (근사값)
// 실제 프로젝트에서는 더 정교한 계산 필요

실전 예제: 홀로그램 효과 #

배운 것들을 조합해서 홀로그램 효과를 만들어봅시다.

import * as THREE from 'three/webgpu';
import {
  color, uv, time, sin, cos,
  positionWorld, cameraPosition,
  normalWorld, reflect, dot,
  uniform, mix
} from 'three/tsl';

// Uniform
const uHologramColor = uniform(new THREE.Color('#00ffff'));
const uScanlineSpeed = uniform(2.0);
const uScanlineCount = uniform(50.0);

// 스캔라인 효과
const scanline = sin(
  positionWorld.y.mul(uScanlineCount).add(time.mul(uScanlineSpeed))
).mul(0.5).add(0.5);

// 프레넬 (가장자리 발광)
const viewDir = cameraPosition.sub(positionWorld).normalize();
const fresnel = dot(normalWorld, viewDir).oneMinus().pow(2);

// 최종 색상
const hologramColor = uHologramColor.mul(fresnel.add(0.2));
const finalColor = hologramColor.mul(scanline.mul(0.5).add(0.5));

// 머티리얼 설정
const material = new THREE.MeshBasicNodeMaterial();
material.colorNode = finalColor;
material.transparent = true;
material.opacity = 0.8;
material.side = THREE.DoubleSide;

프레넬 효과는 시선과 표면이 이루는 각도에 따라 밝기가 변하는 현상입니다. 가장자리가 밝게 빛나는 효과를 만들어줍니다.

GLSL 대비 장점 #

1. 타입 안정성

// GLSL - 런타임에 에러
uniform float uTime;
gl_FragColor = vec4(uTime);  // vec4에 float?

// TSL - 컴파일 타임에 체크
const uTime = uniform(0);
material.colorNode = color(uTime);  // IDE가 경고

2. 조건부 로직

import { If } from 'three/tsl';

// 조건에 따라 다른 쉐이더 적용
const finalColor = If(uv().x.lessThan(0.5),
  color(1, 0, 0),  // 왼쪽 빨강
  color(0, 0, 1)   // 오른쪽 파랑
);

3. 재사용성

// 함수로 추출해서 재사용
function createGradient(colorA, colorB, direction = 'horizontal') {
  const t = direction === 'horizontal' ? uv().x : uv().y;
  return colorA.mix(colorB, t);
}

// 여러 머티리얼에서 사용
material1.colorNode = createGradient(red, blue);
material2.colorNode = createGradient(green, yellow, 'vertical');

주의사항 #

1. 아직 발전 중

TSL은 비교적 새로운 기능입니다. API가 변경될 수 있고, 문서도 부족합니다. Three.js 공식 예제와 소스 코드를 참고하세요.

2. 복잡한 쉐이더는 여전히 GLSL

레이마칭, 볼류메트릭 렌더링 같은 고급 기법은 아직 GLSL이 더 적합합니다. TSL은 진입점으로 좋지만, 결국 GLSL도 배워야 합니다.

3. 디버깅

TSL이 생성한 GLSL/WGSL을 확인하고 싶다면:

console.log(material.fragmentNode.build());

다음 단계 #

TSL로 기본을 익혔다면:

  1. Three.js 공식 예제 - WebGPU 예제들이 대부분 TSL을 사용합니다
  2. MaterialX 노드 - 더 다양한 노이즈, 패턴 함수들
  3. 커스텀 함수 - Fn()으로 복잡한 로직 캡슐화
  4. GLSL 학습 - TSL의 한계를 넘어서려면 결국 필요합니다

GLSL이 두려웠다면, TSL로 시작하세요. JavaScript만 알면 쉐이더의 세계에 입문할 수 있습니다. 나중에 GLSL을 배울 때도 TSL에서 익힌 개념이 도움이 됩니다.