WebGPU / TSL
WebGPU는 GPU에서 그래픽을 렌더링하고 계산을 수행하기 위한 저수준 API를 제공하는 새로운 웹 표준입니다. 이는 WebGL의 후계자로 설계되어 보다 나은 성능과 고급 기능을 제공합니다.
좋은 소식은, 코드베이스에 최소한의 변경 사항으로 이제 Three.js와 함께 사용할 수 있다는 것입니다.
이 강의에서는 WebGPU와 Three.js 및 React Three Fiber를 사용하는 방법과 **Three Shading Language (TSL)**을 사용하여 셰이더를 작성하는 방법을 탐구할 것입니다.
셰이더에 익숙하지 않은 경우, 계속하기 전에 먼저 Shaders 챕터를 완료할 것을 추천합니다.
WebGPU Renderer
WebGL 대신 WebGPU API를 사용하려면, WebGLRenderer 대신 WebGPURenderer
_(Three.js 문서에 전용 섹션 없음)_를 사용해야 합니다.
React Three Fiber를 사용할 때 <Canvas>
컴포넌트를 생성하면 렌더러 설정이 자동으로 수행됩니다. 그러나 <Canvas>
컴포넌트의 gl
prop에 함수를 전달하여 기본 렌더러를 오버라이드할 수 있습니다.
App.jsx
에서 기본 WebGLRenderer
를 사용하는 <Canvas>
컴포넌트가 있습니다. 이를 WebGPURenderer
를 사용하도록 수정해 보겠습니다.
먼저, WebGPURenderer
가 준비될 때까지 frameloop
을 중지해야 합니다. 이를 위해 frameloop
prop을 never
로 설정할 수 있습니다.
// ... import { useState } from "react"; function App() { const [frameloop, setFrameloop] = useState("never"); return ( <> {/* ... */} <Canvas // ... frameloop={frameloop} > {/* ... */} </Canvas> </> ); } export default App;
다음으로, Three.js의 WebGPU 버전을 가져와야 합니다:
import * as THREE from "three/webgpu";
WebGPU를 사용할 때는 기본
three
모듈 대신three/webgpu
모듈을 사용해야 합니다. 이는WebGPURenderer
가 Three.js의 기본 빌드에 포함되어 있지 않기 때문입니다.
그런 다음, gl
prop을 사용하여 새로운 WebGPURenderer
인스턴스를 생성할 수 있습니다:
// ... function App() { const [frameloop, setFrameloop] = useState("never"); return ( <> {/* ... */} <Canvas // ... gl={(canvas) => { const renderer = new THREE.WebGPURenderer({ canvas, powerPreference: "high-performance", antialias: true, alpha: false, stencil: false, shadowMap: true, }); renderer.init().then(() => { setFrameloop("always"); }); return renderer; }} > {/* ... */} </Canvas> </> ); } // ...
canvas
요소를 WebGPURenderer
인스턴스에 전달하여 새로 생성합니다. 또한 powerPreference
, antialias
, alpha
, stencil
, shadowMap
과 같은 렌더러 옵션도 설정합니다. 이 옵션들은 WebGLRenderer
에서 사용하는 옵션과 유사합니다.
마지막으로, 렌더러를 초기화하기 위해 init()
메서드를 호출합니다. 초기화가 완료되면 frameloop
상태를 "always"
로 설정하여 렌더링을 시작합니다.
브라우저에서 결과를 확인해 보세요:
우리의 큐브가 이제 WebGLRenderer
대신 WebGPURenderer
를 사용해서 렌더링되고 있습니다.
이처럼 간단합니다! 우리는 React Three Fiber 애플리케이션에서 WebGPURenderer
를 성공적으로 설정했습니다. 이제 Three.js API를 사용하여 WebGLRenderer
와 동일하게 3D 객체를 생성하고 조작할 수 있습니다.
가장 큰 변화는 셰이더를 작성하는 방법에 있습니다. WebGPU API는 WebGL과 다른 셰이딩 언어를 사용하므로, 셰이더를 다른 방식으로 작성해야 합니다. GLSL 대신 WGSL을 사용합니다.
여기에서 **Three Shading Language (TSL)**이 등장합니다.
Three Shading Language
TSL은 더 사용자 친화적인 방식으로 shaders를 작성하기 위해 Three.js와 함께 사용하도록 설계된 새로운 쉐이딩 언어입니다. 이는 노드 기반 접근 방식을 사용합니다.
TSL의 큰 장점은 렌더러에 상관없이 작동한다는 것입니다. 즉, WebGL 및 WebGPU와 같은 다양한 렌더러에서 동일한 쉐이더를 사용할 수 있다는 것입니다.
이로 인해 두 쉐이딩 언어 간의 차이에 대해 걱정할 필요 없이, 쉐이더를 작성하고 유지하는 것이 더 쉬워집니다.
또한, 새로운 렌더러가 출시되더라도 TSL이 이를 지원하는 한 동일한 쉐이더를 변경 없이 사용할 수 있어 미래 지향적입니다.
Three Shading Language는 아직 개발 중이지만, Three.js의 최신 버전에서 이미 사용 가능합니다. 이를 배우고 변경 사항을 지속적으로 추적하는 가장 좋은 방법은 Three Shading Language 위키 페이지를 확인하는 것입니다. 저는 이를 배우기 위해 많이 사용했습니다.
Node 기반 재료
TSL을 사용하여 쉐이더를 작성하는 방법을 이해하려면, 노드 기반이 무엇을 의미하는지 이해해야 합니다.
노드 기반 접근 방식에서는 다양한 노드를 연결하여 그래프를 생성하여 쉐이더를 작성합니다. 각 노드는 특정 작업이나 함수를 나타내며, 노드 간의 연결은 데이터 흐름을 나타냅니다.
이 접근 방식은 다음과 같은 많은 장점을 제공합니다:
- 시각적 표현: 쉐이더 내 데이터 및 작업 흐름을 이해하고 시각화하기 쉽습니다.
- 재사용성: 다양한 쉐이더에서 사용할 수 있는 재사용 가능한 노드를 만들 수 있습니다.
- 유연성: 노드를 추가하거나 제거하여 쉐이더의 동작을 쉽게 수정하고 변경할 수 있습니다.
- 확장성: 기존 재료에서 기능을 추가/맞춤화하는 것이 간단합니다.
- 비종속성: TSL은 WebGL (GLSL) 또는 WebGPU (WGSL)인지에 관계없이 대상 렌더러에 적합한 코드를 생성합니다.
첫 번째 node-based material을 코딩하기 전에, 온라인 Three.js 플레이그라운드를 사용하여 node-system을 시각적으로 실험할 수 있습니다.
Three.js 플레이그라운드를 열고 상단에서 Examples 버튼을 클릭한 다음, basic > fresnel
을 선택하세요.
두 개의 color
노드와 하나의 float
노드가 fresnel
노드에 부착된 node-based material editor를 볼 수 있어야 합니다. (Color A
, Color B
, 및 Fresnel Factor
)
fresnel
노드는 Basic Material
의 색상에 연결되어 있으며, 찻주전자에 프레넬 효과로 색상이 적용됩니다.
결과를 오른쪽에서 미리 보려면 Splitscreen
버튼을 사용하세요.
예를 들어 Basic Material
의 투명도를 시간에 맞추어 영향을 주고 싶다고 가정해 보겠습니다. Timer
노드를 추가하고 Fract
노드에 연결하여 시간이 1에 도달하면 0으로 리셋합니다. 그런 다음 Basic Material
의 opacity
입력에 연결합니다.
우리의 찻주전자는 이제 사라졌다가 다시 나타나는 효과를 가집니다.
다양한 노드와 그것들이 재료에 어떻게 영향을 미치는지 확인하며 시간을 보내세요.
이제 node-based material의 기본 개념을 이해했으므로, React Three Fiber에서 Three.js의 새로운 node-based material을 사용하는 방법을 살펴보겠습니다.
React Three Fiber 구현
지금까지 WebGL을 사용하면서, 우리는 재질을 만들기 위해 MeshBasicMaterial, MeshStandardMaterial, 심지어 커스텀 ShaderMaterial을 사용해 왔습니다.
WebGPU를 사용할 때 우리는 TSL과 호환되는 새로운 재질들을 사용해야 합니다. 이들의 이름은 이전에 사용했던 것과 동일하며 Material
앞에 Node
가 붙습니다:
MeshBasicMaterial
->MeshBasicNodeMaterial
MeshStandardMaterial
->MeshStandardNodeMaterial
MeshPhysicalMaterial
->MeshPhysicalNodeMaterial
- ...
이를 React Three Fiber에서 선언적으로 사용하기 위해서는, 그들을 extend
해야 합니다. App.jsx
에서:
// ... import { extend } from "@react-three/fiber"; extend({ MeshBasicNodeMaterial: THREE.MeshBasicNodeMaterial, MeshStandardNodeMaterial: THREE.MeshStandardNodeMaterial, }); // ...
향후 React Three Fiber의 버전에서는, 이것이 자동으로 수행될 수 있습니다.
이제 우리는 우리의 컴포넌트에서 새로운 MeshBasicNodeMaterial
과 MeshStandardNodeMaterial
을 사용할 수 있습니다.
우리의 Experience
컴포넌트에서 큐브의 MeshStandardMaterial
을 MeshStandardNodeMaterial
로 교체해봅시다:
<mesh> <boxGeometry args={[1, 1, 1]} /> <meshStandardNodeMaterial color="pink" /> </mesh>
우리는 MeshStandardNodeMaterial
을 MeshStandardMaterial
을 사용하는 것처럼 사용할 수 있습니다.
우리의 큐브는 이제 MeshStandardMaterial
대신 MeshStandardNodeMaterial
에 의존하게 되었으며, 노드를 사용하여 재질을 커스터마이즈할 수 있습니다.
Color Node
TSL을 사용하여 재질을 맞춤화하기 위한 사용자 정의 노드를 만드는 방법을 배워봅시다.
먼저, src/components
폴더에 PracticeNodeMaterial.jsx
라는 새 구성 요소를 만듭니다.
export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { return <meshStandardNodeMaterial color={colorA} />; };
그리고 Experience.jsx
에서 PracticeNodeMaterial
을 사용하여 큐브를 평면으로 교체합니다:
// ... import { PracticeNodeMaterial } from "./PracticeNodeMaterial"; export const Experience = () => { return ( <> {/* ... */} <mesh rotation-x={-Math.PI / 2}> <planeGeometry args={[2, 2, 200, 200]} /> <PracticeNodeMaterial /> </mesh> </> ); };
PracticeNodeMaterial
을 가진 평면이 있습니다.
재질을 맞춤화하기 위해 이제 다양한 노드를 사용하여 다양한 노드를 조정할 수 있습니다. 사용 가능한 노드의 목록은 wiki 페이지에서 찾을 수 있습니다.
우리의 재질 색상을 변경하기 위해 먼저 colorNode
노드를 사용해 봅시다. PracticeNodeMaterial.jsx
내에서:
import { color } from "three/tsl"; export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { return <meshStandardNodeMaterial colorNode={color(colorA)} />; };
우리는 three/tsl
모듈의 color
노드를 사용하여 colorNode
속성을 설정합니다. color
노드는 색상을 인수로 받아서 재질에서 사용할 수 있는 색상 노드를 반환합니다.
이로 인해 이전과 동일한 결과를 얻게 되지만, 이제 재질을 맞춤화하기 위해 더 많은 노드를 추가할 수 있습니다.
three/tsl
모듈에서 mix
와 uv
노드를 가져와서 평면의 UV 좌표를 기반으로 두 색상을 혼합하는데 사용해 봅시다.
import { color, mix, uv } from "three/tsl"; export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { return ( <meshStandardNodeMaterial colorNode={mix(color(colorA), color(colorB), uv())} /> ); };
이것은 재질의 최종 색상을 얻기 위해 다양한 노드를 실행합니다. mix
노드는 두 색상과 비율(이 경우 UV 좌표)을 받아 비율에 따라 두 색상이 혼합된 색상을 반환합니다.
이는 GLSL에서 mix
함수를 사용하는 것과 정확히 동일하지만, 이제 노드 기반 접근 방식을 사용할 수 있습니다. (훨씬 더 읽기 쉬움!)
이제 평면의 UV 좌표에 따라 혼합된 두 색상을 볼 수 있습니다.
놀라운 점은, 우리는 처음부터 시작하는 것이 아닙니다. 기존의 MeshStandardNodeMaterial
을 사용하고 여기에 사용자 정의 노드를 추가하는 것입니다. 따라서 MeshStandardNodeMaterial
의 그림자, 조명 및 기타 모든 기능이 여전히 사용 가능합니다.
간단한 노드 논리를 위해 인라인 노드를 선언하는 것은 괜찮지만, 더 복잡한 논리를 위해서는 useMemo
훅에서 노드(그리고 나중에 유니폼, 기타)를 선언하는 것이 좋습니다:
// ... import { useMemo } from "react"; export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { const { nodes } = useMemo(() => { return { nodes: { colorNode: mix(color(colorA), color(colorB), uv()), }, }; }, []); return <meshStandardNodeMaterial {...nodes} />; };
이는 이전과 정확히 동일한 작업을 수행하되, 이제는 nodes
객체에 더 많은 노드를 추가하고 보다 조직적/일반적인 방식으로 meshStandardNodeMaterial
에 전달할 수 있습니다.
colorA
와 colorB
속성을 변경해도 useMemo
훅 덕분에 쉐이더의 재컴파일이 발생하지 않습니다.
재질의 색상을 변경하기 위해 controls를 추가해 봅시다. Experience.jsx
내에서:
// ... import { useControls } from "leva"; export const Experience = () => { const { colorA, colorB } = useControls({ colorA: { value: "skyblue" }, colorB: { value: "blueviolet" }, }); return ( <> {/* ... */} <mesh rotation-x={-Math.PI / 2}> <planeGeometry args={[2, 2, 200, 200]} /> <PracticeNodeMaterial colorA={colorA} colorB={colorB} /> </mesh> </> ); };
기본 색상은 올바르게 작동하지만 색상을 업데이트해도 효과가 없습니다.
이는 meshStandardNodeMaterial
에 색상을 uniforms
으로 전달해야 하기 때문입니다.
Uniforms
TSL에서 uniforms를 선언하려면, three/tsl
모듈에서 uniform
노드를 사용할 수 있습니다. uniform
노드는 다양한 타입(float
, vec3
, vec4
등)이 될 수 있는 값을 인수로 받고, 컴포넌트 코드에서 업데이트되면서 다른 노드에서 사용할 수 있는 유니폼 노드를 반환합니다.
PracticeNodeMaterial.jsx
에서 하드코딩된 색상을 유니폼으로 변경해 봅시다:
// ... import { uniform } from "three/tsl"; export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { const { nodes, uniforms } = useMemo(() => { const uniforms = { colorA: uniform(color(colorA)), colorB: uniform(color(colorB)), }; return { nodes: { colorNode: mix(uniforms.colorA, uniforms.colorB, uv()), }, uniforms, }; }, []); return <meshStandardNodeMaterial {...nodes} />; };
코드 조직을 더 잘하기 위해 uniforms
객체를 선언하고, 노드를 생성할 때 얻었던 기본 값 대신에 유니폼 값을 사용합니다.
useMemo
에서 리턴함으로써, 컴포넌트에서 유니폼에 접근할 수 있게 되었습니다.
useFrame
에서 유니폼을 업데이트할 수 있습니다:
// ... import { useFrame } from "@react-three/fiber"; export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { // ... useFrame(() => { uniforms.colorA.value.set(colorA); uniforms.colorB.value.set(colorB); }); return <meshStandardNodeMaterial {...nodes} />; };
객체 유니폼을 업데이트할 때는
value.set
메서드를 사용하세요. 예를 들어,color
나vec3
유니폼의 경우입니다.float
유니폼은 값을 직접 설정해야 합니다:uniforms.opacity.value = opacity;
이제 색상이 실시간으로 제대로 업데이트됩니다.
색상을 추가로 조정하기 전에, positionNode
를 사용하여 평면의 정점 위치에 어떻게 영향을 줄 수 있는지 알아봅시다.
Position Node
positionNode
노드는 기하학의 정점 위치에 영향을 줄 수 있게 합니다.
React Three Fiber: The Ultimate Guide to 3D Web Development
✨ You have reached the end of the preview ✨
Go to the next level with Three.js and React Three Fiber!
Get full access to this lesson and the complete course when you enroll:
- 🔓 Full lesson videos with no limits
- 💻 Access to the final source code
- 🎓 Course progress tracking & completion
- 💬 Invite to our private Discord community
One-time payment. Lifetime updates included.