사전 레이 생성 셰이더
개념

레이 생성 셰이더

gabury1고친 사람 github-actions[bot]

레이 생성 셰이더는 광선 추적에서 첫 광선을 쏘는 일을 맡습니다. 화면의 점마다 한 번씩 돌면서 그 점을 지나는 광선을 만들어 쏩니다. 그리고 돌아온 결과를 그 점의 색으로 적습니다.

쉽고 빠른 이해

레이 생성 셰이더는 광선을 어디로 쏠지 정하고, 돌아온 결과로 화면의 점 하나를 칠하는 프로그램입니다. 가로 1920 점, 세로 1080 점 화면이면 이 프로그램이 점마다 하나씩, 200만 번 남짓 돕니다.

이게 없으면 광선이 출발할 곳이 없습니다. 광선이 맞은 물체를 칠하는 프로그램은 따로 있습니다. 하지만 어느 점에서 어느 쪽으로 쏠지, 결과를 어디에 남길지는 이 프로그램만 정합니다.

어떻게 도나:

  1. 광선을 쏘라는 명령이 화면의 점 수만큼 이 프로그램을 띄웁니다
  2. 저마다 자기 점 번호로 광선의 방향을 셈해 쏩니다
  3. 맞은 곳이나 빗나간 곳의 프로그램이 색을 적어 돌려주면, 그 색을 그림에 씁니다

광선 추적으로 그리면 언제나 하나 있어야 합니다. 다만 광선이 가려졌는지만 물을 때는 이것 없이 셰이더 안에서 바로 묻는 방법을 씁니다.

대가는 광선 하나하나가 비싸다는 것입니다. 점마다 광선을 여럿 쏘면 계산도 그 수만큼 늘어납니다. 이웃한 점의 광선이 서로 다른 물체에 맞으면 그래픽 카드가 나란히 돌리기도 어려워집니다.

상세

창문에 모눈종이를 한 장 붙여 둔 화가를 떠올려 봅니다. 화가는 칸 하나를 골라 그 칸 너머를 내다봅니다. 그리고 거기 보인 색을 공책의 같은 칸에 칠합니다. 칸마다 이 일을 되풀이하면 공책에 바깥 풍경이 한 장 그려집니다.

아래 본문에서 모눈종이의 칸은 화면의 점 하나입니다. 칸 너머로 보내는 눈길이 광선입니다. 어느 칸에서 어느 쪽을 볼지 정하고, 본 색을 공책에 칠하는 일이 레이 생성 셰이더의 몫입니다. 눈길이 닿은 곳에 무엇이 있는지 알아내는 일은 다른 셰이더들이 맡습니다.

광선 추적과 셰이더

화면에 보이는 그림은 작은 점 수백만 개로 이루어집니다. 이 점 하나를 픽셀이라고 부릅니다. 그림을 그린다는 것은 픽셀마다 색을 정하는 일입니다.

GPU(Graphics Processing Unit, 그래픽 처리 장치)는 같은 계산을 동시에 아주 많이 돌리도록 만든 칩입니다. 픽셀 수백만 개의 색을 한꺼번에 정하려고 만들었습니다.

셰이더는 GPU 에서 도는 작은 프로그램입니다. 픽셀 하나의 색을 셈하는 식의 계산을 셰이더로 적어 GPU 에 넘깁니다. GPU 는 그 셰이더를 픽셀 수만큼 동시에 돌립니다.

광선 추적은 눈에서 픽셀 하나를 지나는 직선을 쏘아 그 픽셀의 색을 정하는 그리기 방식입니다. 이 직선이 광선입니다. 광선은 영어로 레이(ray)입니다. 그래서 광선 추적을 영어로 레이 트레이싱(ray tracing)이라고 합니다.

광선이 빨간 공에 맞으면 그 픽셀은 빨갛게 칠해집니다. 아무것에도 안 맞으면 배경색이 칠해집니다.

장면의 물체는 대개 작은 삼각형 여럿을 이어 붙여 만듭니다. 둥근 공도 작은 삼각형을 촘촘히 이어 겉면을 덮은 것입니다.

게임 화면을 그리는 흔한 방식은 래스터화입니다. 물체를 하나씩 화면에 눌러 찍어 그 물체가 덮는 픽셀을 칠합니다. 광선 추적은 방향이 반대입니다. 픽셀에서 출발해 물체를 찾아 나섭니다.

아래 그림은 두 방식이 어디서 출발하는지를 위아래로 놓고 견줍니다.

flowchart TD
    subgraph R["래스터화"]
        R1["물체"] --> R2["물체가 덮는 픽셀"] --> R3["그 픽셀을 칠한다"]
    end
    subgraph T["광선 추적"]
        T1["픽셀"] --> T2["눈에서 그 픽셀을 지나는 광선"] --> T3{"빨간 공에 맞나"}
        T3 -->|"맞는다"| T4["빨강으로 칠한다"]
        T3 -->|"안 맞는다"| T5["배경색으로 칠한다"]
    end
    R3 ~~~ T1

한 파이프라인에 묶이는 다섯 셰이더

GPU 에 광선 추적을 시키려면 셰이더 여럿을 실행 전에 한 덩어리로 묶어 둡니다. 이 덩어리가 레이 트레이싱 파이프라인입니다. 광선이 어느 물체에 맞을지는 쏴 봐야 알기 때문에, 쓰일 수 있는 셰이더를 전부 미리 묶습니다.

묶이는 셰이더는 하는 일에 따라 종류가 나뉩니다. 광선을 쏠 때, 물체에 맞았을 때, 빗나갔을 때 저마다 다른 셰이더가 돕니다.

셰이더 언제 도나 하는 일
레이 생성 셰이더 광선을 쏘라는 명령이 내려졌을 때 광선을 만들어 쏘고 결과를 그림에 쓴다
최근접 적중 셰이더 맞은 물체 가운데 가장 가까운 것이 정해진 뒤 그 물체의 색을 셈한다
미스 셰이더 아무것에도 안 맞았을 때 배경색을 정한다
임의 적중 셰이더 물체와 겹칠 때마다 이 겹침을 맞은 것으로 칠지 정한다
교차 셰이더 삼각형이 아닌 모양과 겹치는지 볼 때 겹치는지를 직접 셈한다

다섯 가운데 레이 생성 셰이더만 명령이 직접 띄웁니다. 나머지 넷은 광선이 쏘아진 뒤, 광선이 어디에 닿았느냐에 따라 불립니다. 그래서 레이 생성 셰이더를 이 파이프라인의 시작점이라고 부릅니다.

아래 그림은 누가 누구를 깨우는지 보입니다. 교차 셰이더와 임의 적중 셰이더는 GPU 가 물체를 찾는 도중에 돕니다. 최근접 적중 셰이더와 미스 셰이더는 찾기가 끝난 뒤 둘 가운데 하나만 돕니다.

flowchart TD
    A["레이 생성 셰이더 · 명령이 띄운다"] --> B["광선을 쏜다"]
    B --> C["GPU 의 물체 찾기"]
    C -->|"삼각형이 아닌 모양을 만나면"| D["교차 셰이더"]
    C -->|"물체와 겹칠 때마다"| E["임의 적중 셰이더"]
    C --> F{"맞은 물체가 있나"}
    F -->|"있다"| G["최근접 적중 셰이더"]
    F -->|"없다"| H["미스 셰이더"]

광선이 물체와 겹쳤다고 늘 맞은 것은 아닙니다. 네모 판 하나에 잎사귀 그림을 붙여 잎을 만들었다고 해 봅니다. 광선이 판의 투명한 부분을 지나면 판과 겹치기는 했지만 잎에 맞은 것은 아닙니다. 임의 적중 셰이더가 이런 겹침을 걸러 냅니다.

이 밖에 다른 셰이더가 함수처럼 불러 쓰는 호출 가능 셰이더도 있습니다. 광선과 상관없이 불리므로 표에서는 뺐습니다.

실행 크기와 실행 번호

광선 추적은 프로그램이 GPU 에 광선을 쏘라는 명령을 내리면서 시작합니다. 이 명령은 레이 생성 셰이더를 몇 개 띄울지를 가로·세로·깊이 세 수로 받습니다. 이 세 수를 실행 크기(launch size)라고 부릅니다. 화면을 그릴 때는 대개 가로에 화면 너비, 세로에 화면 높이, 깊이에 1 을 넣습니다.

GPU 는 실행 크기가 만드는 격자의 칸마다 레이 생성 셰이더를 하나씩 띄웁니다. 1920 × 1080 화면이면 2,073,600 개가 뜹니다. 이렇게 뜬 하나하나를 호출(invocation)이라고 부릅니다.

호출마다 자기가 격자의 몇 번째 칸인지 알려 주는 번호를 받습니다. 이 번호를 실행 번호(launch ID)라고 합니다. 레이 생성 셰이더는 이 번호를 보고 자기가 맡을 픽셀을 압니다.

아래 그림은 가로 2, 세로 2 인 아주 작은 화면에 명령을 한 번 내린 모습입니다.

flowchart TD
    A["광선을 쏘라는 명령 · 실행 크기 가로 2 · 세로 2"]
    A --> B0["호출 · 실행 번호 0,0 · 왼쪽 위 픽셀"]
    A --> B1["호출 · 실행 번호 1,0 · 오른쪽 위 픽셀"]
    A --> B2["호출 · 실행 번호 0,1 · 왼쪽 아래 픽셀"]
    A --> B3["호출 · 실행 번호 1,1 · 오른쪽 아래 픽셀"]

넷은 순서 없이 동시에 돕니다. 서로의 결과를 기다리지 않고, 저마다 자기 픽셀 하나만 책임집니다.

실행 크기가 꼭 화면 크기일 필요는 없습니다. 격자의 칸 하나를 픽셀이 아닌 다른 것에 대응시켜도 됩니다. 소리가 벽에 튕기며 퍼지는 길을 찾는 계산처럼, 광선을 쏘는 계산이면 이 격자에 올릴 수 있습니다.

레이 생성 셰이더가 하는 일 셋

호출 하나가 하는 일은 셋입니다. 광선을 만들고, 쏘고, 돌아온 결과를 그림에 씁니다.

첫째, 광선을 만듭니다. 광선은 출발점 하나와 방향 하나로 정해집니다. 출발점은 대개 장면을 바라보는 카메라의 위치입니다. 방향은 카메라에서 자기 픽셀을 지나는 쪽입니다.

둘째, 광선을 쏩니다. 셰이더 안에서 광선을 쏘는 함수를 부르면 GPU 가 광선이 맞을 물체를 찾습니다. 이때 GPU 는 가속 구조를 따라갑니다. 가속 구조는 물체들을 상자에 나눠 담은 트리라서, 광선이 빗나간 상자 안의 물체는 건너뜁니다.

물체를 찾고 나면 맞은 경우는 최근접 적중 셰이더가, 빗나간 경우는 미스 셰이더가 돕니다. 광선에는 결과를 받아 올 작은 상자를 같이 실어 보냅니다. 이 상자를 페이로드라고 부릅니다. 적중 셰이더나 미스 셰이더가 여기에 색을 적습니다.

셋째, 결과를 씁니다. 레이 생성 셰이더는 페이로드에서 색을 꺼내 결과 이미지의 자기 픽셀에 써넣습니다. 이미지는 GPU 메모리에 놓인 픽셀 판입니다.

sequenceDiagram
    participant 생성 as 레이 생성 셰이더
    participant 탐색 as GPU 의 물체 찾기
    participant 적중 as 적중 · 미스 셰이더
    participant 그림 as 결과 이미지
    생성->>탐색: 출발점과 방향으로 광선을 쏜다
    탐색->>적중: 맞은 물체나 빗나감을 알린다
    적중-->>생성: 페이로드에 색을 적어 돌려준다
    생성->>그림: 받은 색을 자기 픽셀에 쓴다

위 넷 가운데 앞의 세 화살표는 광선을 쏘는 함수 한 번 안에서 일어납니다. 레이 생성 셰이더는 그동안 기다렸다가, 함수가 돌아오면 다음 줄부터 이어서 돕니다. 마지막 화살표가 그 다음 줄입니다.

픽셀에서 방향을 셈하는 법

이 소절은 실행 번호 하나가 광선의 방향이 되기까지를 봅니다. 앞 그림의 가로 2, 세로 2 화면에서 오른쪽 아래 픽셀, 곧 실행 번호 (1, 1) 을 예로 씁니다.

단계 셈 값
픽셀 한가운데 잡기 번호에 0.5 를 더한다 (1.5, 1.5)
화면 크기로 나누기 실행 크기 (2, 2) 로 나눈다 (0.75, 0.75)
-1 에서 1 사이로 늘리기 2 를 곱하고 1 을 뺀다 (0.5, 0.5)
세로 뒤집기 세로 값의 부호를 바꾼다 (0.5, -0.5)
앞쪽 붙이기 앞쪽 값 -1 을 셋째 칸에 붙인다 (0.5, -0.5, -1)

0.5 를 더하는 까닭은 광선이 픽셀 모서리가 아니라 가운데를 지나게 하려는 것입니다. 모서리를 지나면 그림 전체가 반 칸씩 밀립니다.

크기로 나누면 화면이 얼마나 크든 0 에서 1 사이 값이 됩니다. 이것을 -1 에서 1 사이로 늘리면 화면 한가운데가 (0, 0) 이 됩니다. 카메라 정면을 기준으로 방향을 셈하기 쉬워집니다.

세로를 뒤집는 까닭은 좌표가 자라는 방향이 달라서입니다. 픽셀 번호는 아래로 갈수록 커집니다. 장면의 좌표는 위로 갈수록 커집니다.

아래는 2 × 2 화면의 네 픽셀이 같은 셈으로 얻는 방향의 가로·세로 값입니다. 위 줄의 세로 값은 양수입니다. 아래 줄은 음수입니다. 화면 위쪽 픽셀이 장면의 위쪽을 보게 된 것입니다.

block-beta
columns 2
  a["실행 번호 (0,0) → 방향 (-0.5, 0.5)"] b["실행 번호 (1,0) → 방향 (0.5, 0.5)"]
  c["실행 번호 (0,1) → 방향 (-0.5, -0.5)"] d["실행 번호 (1,1) → 방향 (0.5, -0.5)"]

마지막 칸의 -1 은 카메라가 바라보는 쪽입니다. 이 예에서는 카메라가 셋째 좌표가 줄어드는 쪽을 바라본다고 정했습니다. 그래서 나온 방향은 오른쪽 아래 앞쪽입니다.

카메라가 장면 안에서 옮겨 가거나 고개를 돌리면, 그만큼 이 방향도 돌려 줍니다. 셈의 뼈대는 바뀌지 않습니다.

짧은 코드 예

아래는 가장 작은 레이 생성 셰이더입니다. 셰이더 언어 GLSL(OpenGL Shading Language, OpenGL 셰이딩 언어)로 적었고, Vulkan 에서 쓰는 형식입니다. 앞 소절의 오른쪽 아래 픽셀을 맡은 호출이 돌 때의 값을 오른쪽 주석에 적었습니다.

glsl
#version 460
#extension GL_EXT_ray_tracing : require

layout(binding = 0)
  uniform accelerationStructureEXT scene;
layout(binding = 1, rgba8)
  uniform image2D img;
layout(location = 0)
  rayPayloadEXT vec3 color;

void main() {
  uvec2 p = gl_LaunchIDEXT.xy;   // (1,1)
  uvec2 n = gl_LaunchSizeEXT.xy; // (2,2)

  vec2 c = vec2(p) + 0.5;  // (1.5,1.5)
  vec2 uv = c / vec2(n);   // (0.75,0.75)
  vec2 d = uv * 2.0 - 1.0; // (0.5,0.5)
  d.y = -d.y;              // (0.5,-0.5)
  vec3 o = vec3(0.0);      // (0,0,0)
  vec3 v = vec3(d, -1.0);  // (0.5,-0.5,-1)

  traceRayEXT(scene,
    gl_RayFlagsOpaqueEXT,  // 광선 설정
    0xFF,                  // 물체 거르기
    0, 1, 0,               // 셰이더 고르기
    o, 0.001,              // 출발점
    v, 1000.0,             // 방향
    0);                    // 페이로드 번호

  vec3 k = color;          // (0.5,0.7,1)
  imageStore(img, ivec2(p), vec4(k, 1.0));
}

첫 두 줄은 이 언어에서 광선 추적 기능을 켜는 선언입니다. 광선 추적은 나중에 더해진 기능이라 켜고 써야 합니다.

layout 으로 시작하는 세 덩어리는 이 셰이더가 쓸 것 셋을 밝힙니다. scene 은 가속 구조입니다. img 는 색을 써넣을 결과 이미지입니다. rgba8 은 빨강·초록·파랑·투명도 네 값을 픽셀마다 한 바이트씩 담는다는 표시입니다.

color 가 페이로드입니다. rayPayloadEXT 가 붙은 변수는 광선에 실어 보낼 상자가 됩니다. 이 예는 빨강·초록·파랑 세 수만 담습니다.

gl_LaunchIDEXT 는 실행 번호입니다. gl_LaunchSizeEXT 는 실행 크기입니다. 둘 다 가로·세로·깊이 세 값을 담습니다. .xy 는 가로와 세로 두 값만 꺼낸다는 뜻입니다. uvec2 는 부호 없는 정수 두 개를 담는 꼴입니다.

빈 줄 다음 여섯 줄 가운데 다섯이 앞 소절 표의 다섯 단계입니다. 주석의 값이 표의 값 열과 같습니다. 남은 한 줄의 o 는 출발점으로, 원점에 둔 카메라 위치입니다.

traceRayEXT 가 광선을 쏘는 함수입니다. 장면과 광선 설정 다음에 물체를 거르는 값이 옵니다. 그다음 세 수는 광선이 맞거나 빗나간 뒤 돌릴 셰이더를 고릅니다. 그런 셰이더는 따로 표에 적어 둡니다. 그 표는 뒤의 「셰이더 바인딩 테이블의 첫 구역」 소절에서 봅니다.

그 뒤로 출발점과 방향이 옵니다. 끝의 0 은 location = 0 으로 밝힌 페이로드를 싣는다는 뜻입니다.

gl_RayFlagsOpaqueEXT 는 모든 물체를 속이 찬 것으로 보라는 설정입니다. 투명한 부분이 없다고 보니 겹침을 걸러 낼 일도 없습니다. 그래서 임의 적중 셰이더를 건너뜁니다.

0xFF 는 비트 여덟 개가 모두 켜진 값입니다. 물체마다 같은 꼴의 여덟 비트 값을 붙여 둡니다. 두 값에 함께 켜진 비트가 하나라도 있어야 광선이 그 물체를 봅니다. 여기서는 여덟 비트를 다 켰으므로 물체 쪽에 켜진 비트가 하나라도 있으면 거르지 않습니다.

출발점 뒤의 0.001 과 방향 뒤의 1000.0 은 광선이 물체를 찾을 거리의 하한과 상한입니다. 하한을 0 보다 조금 크게 두면 광선이 출발한 표면에 곧바로 다시 맞는 일을 막습니다. 상한보다 먼 물체는 안 맞은 것으로 칩니다.

이 예에서는 광선이 아무것에도 안 맞았다고 칩니다. 그러면 미스 셰이더가 돕니다. 미스 셰이더 쪽은 같은 번호의 페이로드를 받아 색을 적습니다.

glsl
#version 460
#extension GL_EXT_ray_tracing : require

layout(location = 0)
  rayPayloadInEXT vec3 color;

void main() {
  color = vec3(0.5, 0.7, 1.0); // 하늘색
}

rayPayloadInEXT 는 광선에 실려 온 상자를 받는다는 표시입니다. 미스 셰이더가 여기 하늘색을 적고 끝나면 레이 생성 셰이더의 traceRayEXT 가 돌아옵니다.

돌아온 뒤 color 에는 하늘색이 들어 있습니다. imageStore 가 그 색을 이미지의 (1, 1) 픽셀에 씁니다. 마지막 1.0 은 투명도로, 비치는 데 없이 칠하라는 뜻입니다.

광선을 여러 번 쏘는 경우

픽셀 하나에 광선 하나만 쏘면 물체 테두리가 계단처럼 보입니다. 광선이 픽셀 한가운데 한 점만 보기 때문입니다. 그래서 픽셀 안에서 출발 위치를 조금씩 흔들어 광선을 여러 개 쏘고 결과를 평균 냅니다. 이런 일을 안티에일리어싱이라고 부릅니다.

반사를 따라갈 때도 광선을 더 쏩니다. 거울에 맞은 광선은 반사 방향으로 다음 광선을 쏴야 거울에 비친 모습이 나옵니다.

다음 광선을 쏘는 방법은 둘입니다. 최근접 적중 셰이더 안에서 광선을 다시 쏘는 방법과, 레이 생성 셰이더가 되풀이문으로 쏘는 방법입니다.

광선을 처리하던 셰이더가 광선을 다시 쏘는 것을 재귀라고 부릅니다. 한 겹 깊어질 때마다 돌아올 곳과 페이로드를 쌓아 둘 메모리가 듭니다. 그래서 파이프라인을 만들 때 몇 겹까지 허용할지 한도를 적습니다. 이 한도를 재귀 깊이라고 합니다.

아래는 거울에 맞은 광선이 한 번 더 튕기는 경우를 재귀로 따라간 모습입니다. 겹이 쌓였다가 거꾸로 풀립니다.

sequenceDiagram
    participant 생성 as 레이 생성 셰이더
    participant 첫 as 최근접 적중 셰이더 · 첫 광선
    participant 둘 as 최근접 적중 셰이더 · 반사 광선
    생성->>첫: 첫 광선을 쏜다
    첫->>둘: 반사 방향으로 다시 쏜다
    Note over 생성,둘: 겹마다 돌아올 곳과 페이로드를 쌓아 둔다 · 몇 겹까지 쌓을지가 재귀 깊이
    둘-->>첫: 색을 돌려준다
    첫-->>생성: 색을 돌려준다

되풀이문으로 쏘면 이 한도에 덜 묶입니다. 최근접 적중 셰이더는 광선을 다시 쏘지 않고, 다음 광선의 출발점과 방향을 페이로드에 적어 돌려줍니다. 레이 생성 셰이더가 그 값을 읽어 다음 광선을 쏩니다. 광선은 언제나 레이 생성 셰이더에서만 나가므로 깊이가 한 겹에 머뭅니다.

flowchart TD
    A["레이 생성 셰이더 · 카메라에서 첫 광선을 쏜다"] --> B["최근접 적중 셰이더 · 색과 다음 방향을 페이로드에 적는다"]
    B --> C{"정한 횟수가 남았고 다음 방향이 있나"}
    C -->|"있다"| D["레이 생성 셰이더 · 페이로드의 방향으로 다시 쏜다"]
    D --> B
    C -->|"없다"| E["레이 생성 셰이더 · 모은 색을 픽셀에 쓴다"]

되풀이는 미리 정한 횟수만큼만 돕니다. 광선이 빗나가 미스 셰이더가 돌면 다음 방향이 없으므로, 횟수가 남아도 거기서 끝납니다.

광선을 여러 번 따라가며 빛이 물체 사이를 튕기는 길을 쫓는 그리기 방식이 패스 트레이싱입니다. 레이 생성 셰이더 안의 이 되풀이문이 그 뼈대가 됩니다.

셰이더 바인딩 테이블의 첫 구역

광선이 맞거나 빗나간 뒤 어느 셰이더를 돌릴지는 셰이더 바인딩 테이블이라는 표가 정합니다. 표는 GPU 메모리에 칸을 줄지어 놓은 것입니다. 칸마다 셰이더 하나를 찾아가는 식별값이 들어 있습니다.

표의 칸은 레이 생성 · 미스 · 적중 · 호출 가능의 네 구역으로 묶입니다. 앞 코드의 「셰이더 고르기」 세 수는 이 가운데 적중 구역과 미스 구역에서 칸을 고릅니다. 앞의 두 수는 적중 구역에서 칸을 셈하는 데 쓰입니다. 셋째 수는 미스 구역의 칸 번호입니다. 이 예에서는 0 번 칸, 곧 하늘색을 적는 미스 셰이더가 돕니다.

레이 생성 구역에는 칸을 여럿 둘 수 있습니다. 화면용 레이 생성 셰이더 하나와 그림자 계산용 하나를 넣어 두는 식입니다.

다만 명령 하나는 레이 생성 셰이더 한 종류만 띄웁니다. 그래서 명령을 내릴 때는 레이 생성 구역 가운데 한 칸의 위치만 넘깁니다. GPU 는 그 칸의 셰이더를 실행 크기만큼 띄웁니다. 둘 다 쓰려면 명령을 두 번 내립니다. 명령마다 다른 칸을 가리키게 합니다.

flowchart TD
    C1["명령 1 · 화면"]
    C2["명령 2 · 그림자"]
    subgraph SBT["셰이더 바인딩 테이블"]
        direction TB
        subgraph RG["레이 생성 구역"]
            RG0["칸 · 화면용"]
            RG1["칸 · 그림자용"]
        end
        subgraph MS["미스 구역"]
            MS0["칸 0 · 하늘색"]
            MS1["칸 1"]
        end
        subgraph HT["적중 구역"]
            HT0["칸 0"]
            HT1["칸 1"]
        end
        subgraph CL["호출 가능 구역"]
            CL0["칸 0"]
        end
    end
    T["셰이더 고르기 세 수"]
    C1 --> RG0
    C2 --> RG1
    RG ~~~ MS
    MS ~~~ HT
    HT ~~~ CL
    T --> MS
    T --> HT

그림에서 명령 둘은 레이 생성 구역의 서로 다른 칸을 가리킵니다. 셰이더 고르기 세 수는 광선이 쏘아진 뒤 미스 구역과 적중 구역의 칸을 고릅니다.

다른 셰이더와 견주기

레이 생성 셰이더를 이웃한 셰이더 둘과 견줘 봅니다. 컴퓨트 셰이더는 그리기와 상관없는 계산을 GPU 에서 돌리는 셰이더입니다. 프래그먼트 셰이더는 래스터화에서 픽셀마다 색을 정하는 셰이더입니다.

프래그먼트 셰이더 컴퓨트 셰이더 레이 생성 셰이더
무엇이 띄우나 삼각형이 덮은 픽셀마다 GPU 가 명령이 정한 격자 명령이 정한 격자
결과를 남기는 법 색을 돌려준다 메모리에 직접 쓴다 이미지나 메모리에 직접 쓴다
적중 · 미스 셰이더를 부르는 광선 못 쏜다 못 쏜다 쏜다

띄우는 방식만 보면 레이 생성 셰이더는 컴퓨트 셰이더와 닮았습니다. 둘 다 명령이 정한 격자의 칸마다 하나씩 뜹니다. 다른 점은 광선을 쏘는 함수로 다른 셰이더를 깨울 수 있느냐입니다.

프래그먼트 셰이더는 색을 돌려주기만 하면 GPU 가 화면에 칠해 줍니다. 레이 생성 셰이더에는 그런 출력이 정해져 있지 않습니다. 이미지에 직접 써넣지 않으면 아무것도 남지 않습니다.

API 마다 부르는 이름

프로그램이 GPU 에 일을 시킬 때 거치는 함수 묶음을 그래픽 API(Application Programming Interface, 애플리케이션 프로그래밍 인터페이스)라고 합니다. 레이 생성 셰이더를 따로 두는 그래픽 API 는 Vulkan 과 Direct3D 12 입니다. Direct3D 12 의 셰이더는 HLSL(High-Level Shading Language, 고수준 셰이딩 언어)로 적습니다.

Vulkan · GLSL Direct3D 12 · HLSL
광선을 쏘라는 명령 vkCmdTraceRaysKHR DispatchRays
실행 번호 gl_LaunchIDEXT DispatchRaysIndex()
실행 크기 gl_LaunchSizeEXT DispatchRaysDimensions()
셰이더 안에서 광선을 쏘는 함수 traceRayEXT TraceRay

이름은 달라도 하는 일은 같습니다. 앞 코드의 줄마다 HLSL 쪽에 짝이 되는 줄이 있습니다.

쓰는 경우와 치르는 비용

광선 추적으로 무엇을 그리든 레이 생성 셰이더는 하나 있어야 합니다. 광선을 처음 쏘는 셰이더가 이것뿐이기 때문입니다. 화면 전체를 광선으로 그릴 때도, 반사나 그림자만 광선으로 보탤 때도 씁니다.

백엔드 개발자가 직접 짤 일은 드뭅니다. 대개 게임 엔진이나 그림을 그리는 프로그램 안쪽에 들어 있습니다.

비용은 셋입니다. 첫째, 광선 하나하나가 비쌉니다. 픽셀마다 광선을 여러 개 쏘면 계산도 그 수만큼 늘어납니다. 화면이 커지면 픽셀 수만큼 또 늘어납니다.

둘째, 이웃한 광선이 갈라지면 느려집니다. GPU 는 이웃한 호출 수십 개를 한 묶음으로 돌립니다. 묶음 안의 호출은 같은 계산 단계를 함께 밟습니다.

이웃한 광선이 서로 다른 물체에 맞아 다른 셰이더를 부르면 묶음이 갈라집니다. 한쪽 셰이더가 도는 동안 다른 쪽 호출은 차례를 기다립니다.

flowchart TD
    A["이웃한 호출 묶음"] --> B{"맞은 물체가 같나"}
    B -->|"같다"| C["한 번에 같은 셰이더를 돌린다"]
    B -->|"다르다"| D["물체 A 의 셰이더 · B 쪽은 기다린다"]
    D --> E["물체 B 의 셰이더 · A 쪽은 기다린다"]

카메라에서 나가는 첫 광선은 방향이 비슷해 덜 갈라집니다. 튕긴 뒤의 광선일수록 많이 갈라집니다.

셋째, 돌릴 수 있는 곳이 좁습니다. 광선 추적을 지원하는 GPU 에서만 레이 트레이싱 파이프라인을 만들 수 있습니다. 지원하지 않는 GPU 에서는 컴퓨트 셰이더로 광선 추적을 흉내 냅니다.

그림자를 그릴 때는 어떤 점에서 빛 쪽으로 쏜 광선이 중간에 물체에 가로막히는지만 알면 됩니다. 이렇게 막혔는지만 물을 때는 파이프라인 전체가 무겁습니다. 이럴 때는 셰이더 안에서 광선을 쏘고 결과를 곧바로 받는 레이 쿼리를 씁니다. 레이 쿼리는 적중 셰이더도 셰이더 바인딩 테이블도 쓰지 않으므로 레이 생성 셰이더도 필요 없습니다.

관련 항목

레이 생성 셰이더와 한 파이프라인에 묶이는 셰이더

최근접 적중 셰이더 · 미스 셰이더 · 임의 적중 셰이더 · 교차 셰이더 · 호출 가능 셰이더 · 적중 그룹 · 셰이더 그룹

레이 생성 셰이더를 띄우는 파이프라인과 명령

레이 트레이싱 파이프라인 · 셰이더 바인딩 테이블 · 셰이더 그룹 핸들 · 커맨드 버퍼 · 파이프라인 레이아웃 · 재귀 깊이 · 재귀

레이 생성 셰이더가 쏜 광선을 GPU 가 따라가는 구조와 알고리즘

가속 구조 · BVH · TLAS · BLAS · 광선 순회 · 광선 교차 판정

레이 생성 셰이더가 광선 방향을 셈할 때 쓰는 좌표와 변환

카메라 · 픽셀 · 정규화 장치 좌표 · 투영 행렬 · 뷰 행렬 · 좌표계

레이 생성 셰이더가 결과를 담고 남기는 자원

광선 페이로드 · 이미지 · 스토리지 이미지 · 디스크립터 세트 · 버퍼 · 메모리

레이 생성 셰이더가 광선을 여러 번 쏘아 그리는 효과

패스 트레이싱 · 안티에일리어싱 · 반사 · 그림자 · 전역 조명 · 앰비언트 오클루전 · 디노이징

레이 생성 셰이더와 견주는 다른 셰이더 단계

컴퓨트 셰이더 · 프래그먼트 셰이더 · 정점 셰이더 · 메시 셰이더 · 태스크 셰이더 · 셰이더

레이 생성 셰이더 대신 광선을 쏘는 수단

레이 쿼리 · 인라인 레이 트레이싱 · 소프트웨어 레이 트레이싱 · 컴퓨트 셰이더

광선 추적과 맞세워지는 그리기 방식

래스터화 · Z 버퍼 알고리즘 · 스크린 공간 반사 · 섀도 맵

레이 생성 셰이더를 정의하는 그래픽 API와 셰이더 언어

API · Vulkan · Direct3D 12 · DXR · GLSL · HLSL · SPIR-V · Metal

레이 생성 셰이더를 돌리는 하드웨어와 실행 방식

GPU · 병렬 처리 · 워프 · 분기 발산 · SIMT · 셰이더 코어

레이 생성 셰이더가 속하는 상위 분류

광선 추적 · 그래픽스 · 렌더링 · 실시간 렌더링 · 3D 그래픽스

다른 이름: ray generation shader · raygen shader · 광선 생성 셰이더 · 레이 제너레이션 셰이더 · 레이젠 셰이더