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로 기본을 익혔다면:
- Three.js 공식 예제 - WebGPU 예제들이 대부분 TSL을 사용합니다
- MaterialX 노드 - 더 다양한 노이즈, 패턴 함수들
- 커스텀 함수 - Fn()으로 복잡한 로직 캡슐화
- GLSL 학습 - TSL의 한계를 넘어서려면 결국 필요합니다
GLSL이 두려웠다면, TSL로 시작하세요. JavaScript만 알면 쉐이더의 세계에 입문할 수 있습니다. 나중에 GLSL을 배울 때도 TSL에서 익힌 개념이 도움이 됩니다.
- Previous: 포스트 프로세싱 과욕 - 효과 5개가 3개보다 못한 이유