쉐이더 소개

Starter pack

드디어 쉐이더의 세계에 발을 들일 시간이 되었습니다. 쉐이더는 다양한 시각적 효과를 만들어내는 데 필수적입니다. 이번 장에서는 쉐이더에 대해 배우고, 그것을 통해 무엇을 이룰 수 있는지, 그리고 React Three Fiber에서 어떻게 사용할 수 있는지를 알아보겠습니다.

서문

시작하기 전에, 쉐이더에 익숙해지기까지 시간이 걸릴 수 있다는 점을 언급하고 싶습니다. 이것은 우리가 지금까지 작성한 코드와는 다르게 작동합니다. 시각적 효과를 창조하는 새로운 사고방식이 필요합니다. 하지만 걱정하지 마세요. 제가 여러분을 안내할 것이며, 기초부터 시작해서 점차 고급 주제로 넘어갈 것입니다.

처음에 모든 것을 이해하지 못하더라도 낙담하지 마세요. 이것은 새로운 개념이며, 익숙해지기 위해서는 연습이 필요합니다. 시간을 충분히 가지며 실험하고 연습하는 것을 추천합니다. 하지만 분명히 가치가 있다고 약속합니다! 쉐이더는 매우 강력하며, 여러분이 상상할 수 있는 어떤 시각적 효과도 만들 수 있는 제어력을 제공합니다.

게다가, 우리는 모두 다른 학습 스타일을 가지고 있습니다. 어떤 사람은 읽으면서, 다른 사람은 동영상을 보면서, 또 다른 사람은 직접 해보면서 더 잘 배울 수 있습니다. 이 주제에 대해서는 다양한 출처를 참고하는 것이 매우 유익할 수 있습니다. 이 장의 끝에서 여러분의 지식을 강화하고 우리가 여기서 다루는 내용을 넘어설 수 있는 자료를 공유하겠습니다.

여러분이 겁먹지 않았고 쉐이더에 대해 배우는 것에 대해 흥분하고 있기를 바랍니다. 그럼 시작해봅시다!

쉐이더란 무엇인가?

쉐이더는 GPU (그래픽 처리 장치)에서 실행되는 작은 프로그램입니다. 이것은 C와 유사한 GLSL (OpenGL Shading Language)이라는 언어로 작성됩니다.

쉐이더는 메쉬의 버텍스를 위치시키는 데 사용되는 버텍스 쉐이더와 얼굴의 각 픽셀에 색상을 입히는 데 사용되는 프래그먼트 쉐이더로 나뉩니다.

사실, 우리들은 이미 계속해서 쉐이더를 사용해 왔습니다. 우리가 material을 생성할 때마다, 우리는 쉐이더를 사용하는 것입니다. 예를 들어, MeshBasicMaterial을 생성할 때 우리는 메쉬를 단색으로 칠하는 쉐이더를 사용하고 있습니다. MeshStandardMaterial을 생성할 때는 조명, 그림자, 반사를 시뮬레이트하는 쉐이더를 사용합니다.

버텍스 쉐이더

버텍스 쉐이더는 기하의 각 버텍스에 대해 실행되는 프로그램입니다. 주요 책임은 3D 공간 (우리의 3D 세계)의 버텍스를 2D 공간 (화면 또는 뷰포트)으로 변환하는 것입니다. 이 변환은 여러 행렬을 사용하여 이루어집니다:

  • 뷰 행렬: 이 행렬은 장면 내에서 카메라의 위치와 방향을 나타냅니다. 버텍스를 월드 공간에서 카메라 공간으로 변환합니다.
  • 투영 행렬: 이 행렬은 원근 또는 직교 투영으로, 버텍스를 카메라 공간에서 정규 기기 좌표(NDC)로 변환하여 2D 화면에 최종 투영을 준비합니다.
  • 모델 행렬: 이 행렬은 장면 내 개별 객체의 위치, 회전 및 크기를 포함합니다. 버텍스를 객체 공간에서 월드 공간으로 변환합니다.

또한, 버텍스 쉐이더는 버텍스의 원래 위치와 그것과 관련된 다른 속성도 통합합니다.

버텍스 쉐이더의 개요도

기하의 각 버텍스에 대해 버텍스 쉐이더가 실행됩니다.

마지막으로, 변환된 2D 공간에서의 버텍스 위치는 사전 정의된 변수 gl_Position을 통해 반환됩니다. 모든 버텍스가 변환된 후, GPU는 그들 사이의 값을 보간하여 기하의 얼굴을 생성하고, 이를 레스터화한 후 화면에 렌더링합니다.

프래그먼트 쉐이더

프래그먼트 쉐이더는 픽셀 쉐이더로도 알려져 있으며, 래스터화 프로세스에 의해 생성된 각 프래그먼트(또는 픽셀)에 대해 실행되는 프로그램입니다. 주된 작업은 화면의 각 픽셀의 최종 색상을 결정하는 것입니다.

프래그먼트 쉐이더의 개요

래스터화 동안 생성된 각 프래그먼트에 대해 프래그먼트 쉐이더가 실행됩니다.

프래그먼트 쉐이더는 정점 쉐이더로부터 색상, 텍스처 좌표, 노멀 및 지오메트리의 정점과 관련된 기타 속성과 같은 인터폴레이션된 값을 수신합니다. 이러한 인터폴레이션된 값을 varyings라 하며, 각 프래그먼트 위치에서의 표면 특성에 대한 정보를 제공합니다.

인터폴레이션된 값 외에도 프래그먼트 쉐이더는 텍스처 샘플링과 모든 프래그먼트에 공통으로 사용되는 uniform 변수를 액세스할 수 있습니다. 이러한 uniform 변수는 조명 위치, 물질 속성 또는 쉐이딩 계산에 필요한 기타 데이터를 나타낼 수 있습니다.

이번 레슨에서는 나중에 attributesuniforms에 대해 다시 다루겠습니다.

입력 데이터를 사용하여 프래그먼트 쉐이더는 프래그먼트의 최종 색상을 결정하기 위해 다양한 계산을 수행합니다. 이는 복잡한 조명 계산, 텍스처 매핑, 쉐이딩 효과 또는 장면에서 원하는 기타 시각적 효과를 포함할 수 있습니다.

색상 계산이 완료되면, 프래그먼트 쉐이더는 gl_FragColor라는 사전 정의된 변수를 사용하여 프래그먼트의 최종 색상을 출력합니다.

가능한 쉽게 쉐이더를 설명하려고 노력했으며, 일부 기술적 세부 사항은 의도적으로 생략했지만 여전히 추상적으로 느껴질 수 있다는 것을 이해합니다. 간단한 쉐이더를 만들어 실제로 어떻게 작동하는지 알아봅시다.

첫번째 쉐이더

스타터 팩을 실행해봅시다. 화면 중앙에 검은 평면이 있는 프레임이 보일 것입니다:

검은 평면이 있는 프레임

ShaderPlane.jsx 파일을 열어보면, 평면 지오메트리와 기본 material을 가진 간단한 mesh가 포함되어 있습니다. 이 material을 사용자 정의된 쉐이더 material로 교체할 것입니다.

shaderMaterial

쉐이더 material을 생성하기 위해 우리는 Drei 라이브러리shaderMaterial 함수를 사용합니다.

이 함수는 3개의 매개변수를 가집니다:

  • uniforms: 쉐이더에서 사용되는 uniform 변수를 포함하는 객체. 현재는 비어둡니다.
  • vertexShader: 정점 쉐이더에 대한 GLSL 코드 문자열.
  • fragmentShader: 프래그먼트 쉐이더에 대한 GLSL 코드 문자열.

파일 상단에 MyShaderMaterial이라는 새로운 쉐이더 material을 선언합시다:

import { shaderMaterial } from "@react-three/drei";

const MyShaderMaterial = shaderMaterial(
  {},
  `
  void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }`,
  `
  void main() {
    gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
  }
  `
);

잠시 후에 쉐이더 코드에 대해 자세히 설명하겠습니다.

React Three Fiber에서 선언적으로 사용할 수 있도록 extend 메서드를 사용합니다:

import { extend } from "@react-three/fiber";
// ...

extend({ MyShaderMaterial });

이제 <meshBasicMaterial>을 새로운 쉐이더 material로 교체할 수 있습니다:

import { shaderMaterial } from "@react-three/drei";
import { extend } from "@react-three/fiber";

const MyShaderMaterial = shaderMaterial(
  {},
  `
  void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }`,
  `
  void main() {
    gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
  }
  `
);

extend({ MyShaderMaterial });

export const ShaderPlane = ({ ...props }) => {
  return (
    <mesh {...props}>
      <planeGeometry args={[1, 1]} />
      <myShaderMaterial />
    </mesh>
  );
};

이전과 동일한 검은 평면이 보여야 합니다. 아직 아무 것도 변경하지 않았지만, 이제 우리는 사용자 정의 쉐이더 material을 사용하고 있습니다.

작동 중임을 확인하기 위해, 프래그먼트 쉐이더에서 반환하는 색상을 변경해봅시다. gl_FragColor 라인을 다음과 같이 교체하세요:

gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);

gl_FragColor는 프래그먼트의 색상을 나타내는 사전 정의된 변수입니다. 이 변수는 색상의 red, green, blue, alpha 채널을 나타내는 4개의 구성 요소를 가진 vec4입니다. 각 구성 요소는 0과 1 사이의 float입니다.

첫 번째 구성 요소를 1.0으로 설정하면, 빨강 채널을 최대값으로 설정하여 빨간색이 나타나게 됩니다.

화면 중앙에 빨간 평면이 보여야 합니다:

빨간 평면이 있는 프레임

축하합니다! 첫 번째 쉐이더 material을 생성하셨습니다. 간단하지만 시작입니다.

셰이더 코드

다음으로 넘어가기 전에, 셰이더를 더 편하게 작성할 수 있도록 개발 환경을 설정해 보겠습니다.

셰이더 코드를 작성하는 방법에는 두 가지가 있습니다:

  • 인라인: JavaScript 파일 내에 직접 셰이더 코드를 작성할 수 있습니다.
  • 외부 파일: .glsl 확장자를 가진 별도의 파일에 셰이더 코드를 작성하고 이를 JavaScript 파일에서 가져올 수 있습니다.

저는 보통 적절한 material 파일 내에서 인라인 방식을 선호합니다. 이렇게 하면 셰이더 코드가 material 선언과 가까워집니다.

그러나 작성하고 읽기 편하게 하기 위해, GLSL문법 강조 기능을 사용하는 것을 추천합니다. Visual Studio Code의 Comment tagged templates 확장을 사용할 수 있습니다. 이 확장은 템플릿 문자열 내부의 GLSL 코드를 강조 표시해 줍니다.

GLSL 문법 강조기

설치 후, 문법 강조 기능을 활성화하려면 셰이더 코드 시작 부분에 다음 주석을 추가해야 합니다:

const MyShaderMaterial = shaderMaterial(
  {},
  /* glsl */ `
  void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }`,
  /* glsl */ `
  void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
  `
);

템플릿 문자열 안의 GLSL 코드가 강조 표시된 것을 볼 수 있습니다:

문법 강조 기능 동작 중

위쪽의 vertex shader가 이제 올바른 문법 강조 표시가 되어 읽기 쉬워졌습니다.

이것으로 인라인 셰이더 코드는 완료되었습니다. 셰이더 코드를 별도로 관리하고 싶다면 외부 파일을 사용할 수도 있습니다. 방법을 살펴보겠습니다.

GLSL 파일 임포트하기

먼저 src 폴더 안에 shaders라는 새로운 폴더를 만드세요. 이 폴더 안에 myshader.vertex.glsl 파일과 myshader.fragment.glsl 파일을 생성하고, 각각의 셰이더 코드를 복사해서 넣으세요.

myshader.vertex.glsl:

void main() {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

myshader.fragment.glsl:

void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

다양한 셰이더가 있는 경우, 선호하는 이름 규칙을 사용하거나 셰이더를 하위 폴더로 그룹화하여 조직할 수 있습니다.

그런 다음, JavaScript 파일에서 이러한 파일을 임포트할 수 있도록 개발 종속성으로 vite-plugin-glsl 플러그인을 설치해야 합니다:

yarn add vite-plugin-glsl --dev

그런 다음 vite.config.js 파일에서 이 플러그인을 임포트하고 plugins 배열에 추가하세요:

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import glsl from "vite-plugin-glsl";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), glsl()],
});

이제 GLSL 파일을 JavaScript 파일에 임포트하여 셰이더 코드로 사용할 수 있습니다:

import myShaderFragment from "./shaders/myshader.fragment.glsl";
import myShaderVertex from "./shaders/myshader.vertex.glsl";

const MyShaderMaterial = shaderMaterial({}, myShaderVertex, myShaderFragment);

이제 셰이더 코드를 편하게 작성하고 임포트하는 방법을 알았으니, 셰이더 코드의 다양한 부분을 탐색할 수 있습니다.

GLSL

셰이더 코드는 GLSL(OpenGL Shading Language)로 작성됩니다. 이는 C와 유사한 언어로, 기초를 살펴보겠습니다.

타입

GLSL에는 여러 타입이 있지만 가장 일반적인 것은 다음과 같습니다:

  • bool: 부울 값 (true 또는 false).
  • int: 정수.
  • float: 부동소수점 숫자.
  • vectors: 숫자의 모음. vec2는 2개의 부동소수점 숫자(xy)의 모음이고, vec3는 3개의 부동소수점 숫자(x, y, z)의 모음이며, vec4는 4개의 부동소수점 숫자(x, y, z, w)의 모음입니다. x, y, z, w 대신 색상에 대해 r, g, b, a를 사용할 수도 있습니다. 상호 교환 가능합니다.
  • matrices: 벡터의 모음. 예를 들어, mat2는 2개의 벡터의 모음이고, mat3는 3개의 벡터의 모음이며, mat4는 4개의 벡터의 모음입니다.

스위즐링과 조작

스위즐링을 사용하여 벡터의 구성 요소에 접근할 수 있습니다. 예를 들어, 다른 벡터의 구성 요소를 사용하여 새로운 벡터를 생성할 수 있습니다:

vec3 a = vec3(1.0, 2.0, 3.0);
vec2 b = a.xy;

이 예에서, baxy 구성 요소를 가진 벡터가 됩니다.

또한 스위즐링을 사용하여 구성 요소의 순서를 변경할 수 있습니다:

vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = a.zyx;

이 예에서, bvec3(3.0, 2.0, 1.0)과 같아집니다.

모든 구성 요소가 동일한 새로운 벡터를 생성하려면 생성자를 사용할 수 있습니다:

vec3 a = vec3(1.0);

이 예에서, avec3(1.0, 1.0, 1.0)과 같습니다.

연산자

GLSL은 일반적인 산술 연산자를 제공합니다: +, -, *, /, +=, /=, *= 및 일반적인 비교 연산자: ==, !=, >, <, >=, <=.

이들은 올바른 유형과 함께 사용해야 합니다. 예를 들어, 정수를 실수에 더할 수는 없으며, 먼저 정수를 실수로 변환해야 합니다:

int a = 1;
float b = 2.0;
float c = float(a) + b;

벡터와 행렬에서도 연산을 수행할 수 있습니다:

vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = vec3(4.0, 5.0, 6.0);
vec3 c = a + b;

이는 다음과 동일합니다:

vec3 c = vec3(a.x + b.x, a.y + b.y, a.z + b.z);

함수

버텍스 셰이더와 프래그먼트 셰이더의 진입점은 main 함수입니다. 이 함수는 셰이더가 호출될 때 실행됩니다.

void main() {
  // 여기에 코드 작성
}

void는 함수의 반환 유형입니다. 함수가 아무것도 반환하지 않음을 의미합니다.

사용자 정의 함수를 정의할 수도 있습니다:

float add(float a, float b) {
  return a + b;
}

그런 다음 main 함수에서 이 함수를 호출할 수 있습니다:

void main() {
  float result = add(1.0, 2.0);
  // ...
}

GLSL은 sin, cos, max, min, abs, round, floor, ceil과 같은 공통 작업을 위한 많은 내장 함수를 제공하며 mix, step, length, distance 등 여러 유용한 함수들도 제공합니다.

다음 레슨에서 필수 함수들을 발견하고 함께 실습해 보겠습니다.

루프와 조건문

GLSL은 for 루프와 if 문을 지원합니다. 이는 JavaScript와 유사하게 작동합니다:

for (int i = 0; i < 10; i++) {
  // 여기에 코드를 작성하세요
}

if (condition) {
  // 여기에 코드를 작성하세요
} else {
  // 여기에 코드를 작성하세요
}

로깅 / 디버깅

셰이더 프로그램은 각 버텍스와 프래그먼트 별로 병렬로 실행되므로 console.log를 사용하여 코드를 디버그하거나 breakpoint를 추가할 수 없습니다. 이것이 셰이더를 디버그하는 것을 어렵게 만드는 이유입니다.

셰이더를 디버그하는 일반적인 방법은 gl_FragColor를 사용하여 변수의 값을 시각화하는 것입니다.

컴파일 오류

셰이더 코드에서 실수를 하면 콘솔에 컴파일 오류가 나타납니다. 오류의 줄과 유형을 알려주며, 항상 이해하기 쉬운 것은 아니지만 문제를 어디서 찾아야 할지 알 수 있는 좋은 방법입니다.

gl_FragColor에서 알파 채널을 제거하고 무슨 일이 일어나는지 봅시다:

void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0);
}

콘솔에서 컴파일 오류를 볼 수 있습니다:

Compilation error

gl_FragColor가 4개의 구성 요소를 기대하지만 우리는 3개만 제공했다고 말하고 있습니다.

오류를 제거하기 위해 알파 채널을 1.0으로 복원하는 것을 잊지 마세요.

Uniforms

JavaScript 코드에서 셰이더로 데이터를 전달하기 위해 uniforms를 사용합니다. 이들은 모든 vertex와 fragment에서 일정하게 유지되는 변수들입니다.

projectionMatrix, modelViewMatrix, 그리고 position은 셰이더로 자동으로 전달되는 기본 제공 uniforms의 예입니다.

이제 셰이더에 색상을 전달하기 위한 커스텀 uniform을 만들어 보겠습니다. 이를 plane을 색칠하는 데 사용할 것입니다. 이 uniform을 uColor라고 부를 것입니다. 일반적으로 코드를 명확하게 하기 위해 uniform의 이름에 u를 접두사로 붙이는 것이 좋습니다.

먼저, shaderMaterial의 uniforms 객체에서 이를 선언합니다:

import { Color } from "three";
// ...
const MyShaderMaterial = shaderMaterial(
  {
    uColor: new Color("pink"),
  }
  // ...
);

// ...

그런 다음, fragment 셰이더에서 이를 사용할 수 있습니다:

uniform vec3 uColor;

void main() {
  gl_FragColor = vec4(uColor, 1.0);
}

plane이 분홍색으로 색칠된 것을 볼 수 있어야 합니다:

A frame with a pink plane

여기서, 분홍색은 uniform의 기본 값입니다. 우리는 material에서 이를 직접 변경할 수 있습니다:

<MyShaderMaterial uColor={"lightblue"} />

A frame with a light blue plane

plane이 이제 연한 파란색으로 색칠되었습니다.

vertex와 fragment 셰이더 모두에서 uniforms에 접근할 수 있습니다. 이제 시간을 vertex 셰이더에 두 번째 uniform으로 추가하여 plane을 위아래로 움직이게 해보겠습니다:

Three.js logoReact logo

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
Unlock the Full Course – Just $85

One-time payment. Lifetime updates included.