소개
React-Three-Fiber(이하 R3F)는 React를 사용하여 Three.js로 3D 그래픽을 만들 수 있는 강력한 라이브러리입니다. TSL(Three.js Shading Language)은 Three.js에서 셰이더를 작성하는 데 사용되는 언어로, GLSL과 유사하지만 Three.js에 특화된 기능을 제공합니다. 이 글에서는 R3F 프로젝트에서 TSL을 설정하고 사용하는 방법을 단계별로 설명합니다.
R3F 프로젝트에 TSL 설정하기
참조: https://r3f.docs.pmnd.rs/api/canvas#webgpu
먼저 R3F 프로젝트에 TSL을 설정하는 방법을 살펴보겠습니다.
import * as THREE from 'three/webgpu';
import * as TSL from 'three/tsl';
import { Canvas, extend, useFrame, useThree } from '@react-three/fiber';
declare module '@react-three/fiber' {
interface ThreeElements extends ThreeToJSXElements<typeof THREE> {}
}
extend(THREE as any);
export default () => (
<Canvas
gl={async (props) => {
const renderer = new THREE.WebGPURenderer(props as any);
await renderer.init();
return renderer;
}}
>
<mesh>
<meshBasicNodeMaterial />
<boxGeometry />
</mesh>
</Canvas>
);
위 코드에서는 three/webgpu와 three/tsl 모듈을 가져오고, R3F의 extend 함수를 사용하여 Three.js의 WebGPU 렌더러를 확장합니다.
그런 다음 Canvas 컴포넌트의 gl 속성에 비동기 함수를 전달하여 WebGPU 렌더러를 초기화합니다.
TLS 개요
참조: https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language
TSL은 Three.js에서 셰이더를 작성하는 데 사용되는 언어로, GLSL과 유사하지만 Three.js에 특화된 기능을 제공합니다.
TSL의 주요 특징은 다음과 같습니다:
-
JavaScript 기반 셰이더 작성
- 문자열 조작 없이 JS/TS로 직접 셰이더 로직 작성
- NPM, import/export 등 JS 생태계 활용 가능
- TypeScript 타입 체킹 지원
-
렌더러 독립성
- WebGPU(WGSL)와 WebGL(GLSL) 모두 지원
- 동일한 TSL 코드가 자동으로 양쪽 백엔드로 변환됨
-
자동 최적화
- 반복되는 표현식 자동 캐싱
- 필요한 경우에만 varying 생성
- Uniform/attribute 자동 재사용
-
노드 시스템
- 모든 TSL 컴포넌트는 Node 클래스를 확장
- 노드들은 자동으로 연결되고 한 번만 계산됨
- Tree shaking 지원
TLS 코드 분석
1. Uniform 변수 선언
const sliceStart = TSL.uniform(1.75); // 슬라이스 시작 각도 (라디안)
const sliceArc = TSL.uniform(1.25); // 슬라이스 각도 범위 (라디안)
const sliceColor = TSL.uniform(TSL.color('#0096ff')); // 단면 색상 (파란색)
슬라이스 영역: 1.75 라디안 위치에서 시작해서 1.25 라디안만큼의 부채꼴 영역을 정의하는 uniform 변수들입니다.
JS/TS에서 GPU로 변경 가능한 uniform 값들을 선언하는 방법을 보여줍니다.
예제는 Leva를 사용하여 이러한 uniform 값들을 쉽게 조정할 수 있습니다.
2. inAngle 함수 - 각도 범위 체크
const inAngle = TSL.Fn<[position: THREE.Node, angleStart: number, angleArc: number]>(
([position, angleStart, angleArc]) => {
// 1. 2D 위치로부터 각도 계산
const angle = TSL.atan(position.y, position.x)
.sub(angleStart) // 시작 각도만큼 빼기
.mod(TSL.float(Math.PI * 2)) // 0 ~ 2π 범위로 정규화
.toVar();
// 2. 각도가 슬라이스 범위 안에 있는지 체크
return angle.greaterThan(0).and(angle.lessThan(angleArc));
}
);
동작 원리:
Y
|
| 슬라이스 영역
| /
|/_____ angleArc
/|
/ |
/__|_________ X
angleStart
위치 -> 각도 계산 -> 범위 체크 -> true/false 반환
3. output 함수 - 메인 렌더링 로직
const output = TSL.Fn(() => {
// 1. 슬라이스 영역 내부면 픽셀 제거 (구멍 뚫기)
inAngle(TSL.positionLocal.xy, sliceStart, sliceArc).discard();
// 2. 기본 출력 가져오기
const finaleOutput = TSL.output;
// 3. 뒷면(단면)이면 파란색으로 칠하기
TSL.If(TSL.frontFacing.not(), () => {
finaleOutput.assign(TSL.vec4(sliceColor, 1));
});
return finaleOutput;
})();
- .discard(): 슬라이스 영역 내부 픽셀은 렌더링하지 않음 (구멍)
- frontFacing.not(): 잘린 단면(뒷면)을 감지
- 단면 착색: 뒷면에만 파란색 적용
4. castShadow 함수 - 그림자 처리
const castShadow = TSL.Fn(() => {
// 슬라이스 영역은 그림자도 투명하게
inAngle(TSL.positionLocal.xy, sliceStart, sliceArc).discard();
return TSL.vec4(0, 0, 0, 1); // 검은색 그림자
})();
- 그림자 계산에서도 동일하게 슬라이스 영역 제거
- 일관된 시각적 효과 유지
마치며
TSL은 R3F 프로젝트에서 셰이더를 작성하는 강력한 도구입니다.
조건문, 반복문, 함수 등을 함수형 프로그래잉 스타일로 작성하기 때문에 다소 어색한 것은 사실입니다.
하지만, GLSL 코드의 오류를 잡아본 경험이 있으신 분들은 이 방식이 가져다줄 안정성과 유지보수성의 이점을 금방 이해하실 수 있을 것입니다.
다음에는 더 기초적이고 단순한 TSL 예제를 다뤄보도록 하겠습니다.