트랜스폼 피드백
고친 사람 github-actions[bot]
트랜스폼 피드백은 그래픽 카드가 옮긴 꼭짓점들의 새 위치를 메모리에 받아 적습니다. 받아 적은 값은 다음 그리기에서 입력으로 다시 들어갑니다. 한 번 한 계산을 버리지 않고 이어 쓰게 해 줍니다.
쉽고 빠른 이해
그래픽 카드는 도형의 꼭짓점을 옮기는 계산을 하고 곧바로 화면에 칠합니다. 트랜스폼 피드백은 그 계산 결과를 칠하기 전에 메모리로 한 벌 빼 둡니다. 연기 입자 만 개를 한 번 움직였다면 그 새 위치가 메모리에 남습니다.
이게 없으면 계산 결과는 화면에 칠해지고 사라집니다. 다음 화면에서 이어 움직이려면 같은 계산을 처음부터 다시 해야 합니다.
어떻게 도나:
- 계산 결과 중 무엇을 적을지 미리 정해 둡니다
- 적을 메모리를 연결합니다. 기록을 켠 채 그리기를 부릅니다
- 다음 그리기가 그 메모리를 꼭짓점 목록으로 읽어 갑니다
대가는 메모리 크기를 미리 잡아야 한다는 것입니다. 모자라면 넘친 뒷부분은 오류 없이 버려집니다. 값을 들어온 순서대로만 적을 수 있어서, 아무 칸에나 값을 써야 하는 계산에는 맞지 않습니다.
상세
소스를 끓이는 요리사가 있습니다. 끓인 소스를 접시에 담아 내보내면서 한 국자를 따로 떠서 냉장고에 넣어 둡니다. 다음 요리는 그 한 국자에서 시작합니다. 소스를 처음부터 다시 끓이지 않아도 됩니다.
그래픽스 파이프라인
정점은 도형을 이루는 꼭짓점 하나입니다. 위치와 색 같은 값이 붙어 다닙니다.
프리미티브는 정점을 묶어 만든 가장 작은 도형입니다. 점 하나, 선분 하나, 삼각형 하나가 각각 프리미티브입니다. 삼각형 프리미티브 하나는 정점 셋으로 이루어집니다. 캐릭터 같은 물체는 삼각형 프리미티브 여럿을 이어 붙여 만듭니다.
그래픽 카드에서 계산을 맡는 칩이 GPU(Graphics Processing Unit, 그래픽 처리 장치)입니다. GPU 에는 작은 계산 장치가 아주 많이 들어 있습니다. 그래서 같은 계산을 정점 수천 개에 한꺼번에 돌릴 수 있습니다.
프로그램은 GPU 를 직접 만지지 않습니다. 정해진 함수 묶음을 불러 그리기를 맡깁니다. 이 함수 묶음이 그래픽 API(Application Programming Interface, 응용 프로그래밍 인터페이스)입니다.
OpenGL 은 대표적인 그래픽 API 입니다. 트랜스폼 피드백은 OpenGL 에서 이 기능을 부르는 이름입니다. 이 글의 함수 이름도 모두 OpenGL 의 것입니다.
GPU 는 정점을 받아 화면에 칠하기까지 정해진 단계를 차례로 밟습니다. 이 단계들의 줄이 그래픽스 파이프라인입니다. 줄여서 파이프라인이라고도 합니다.
파이프라인의 맨 앞 단계는 메모리에서 정점을 읽어 옵니다. 그 뒤 몇몇 단계에는 개발자가 짧은 프로그램을 꽂습니다. 이 프로그램이 셰이더입니다. 정점을 어디로 옮길지, 어떤 색으로 칠할지를 셰이더가 정합니다.
정점 셰이더는 정점마다 한 번씩 돌며 새 위치를 계산하는 셰이더입니다. 캐릭터가 걸을 때 다리의 정점을 옮기는 계산이 이 단계에서 돕니다.
정점을 옮기는 이 계산이 변환, 영어로 트랜스폼(transform)입니다. 트랜스폼 피드백이라는 이름의 앞말이 여기서 왔습니다.
지오메트리 셰이더는 정점 셰이더 뒤에 붙일 수 있는 단계입니다. 프리미티브 하나를 받아 여러 개로 늘리거나 버립니다. 필요 없으면 비워 둡니다.
래스터화는 프리미티브가 화면의 어느 칸을 덮는지 골라내는 단계입니다. 화면을 이루는 칸 하나하나가 픽셀입니다. 래스터화 뒤의 단계들이 픽셀마다 색을 정해 화면에 칠합니다.
래스터화를 지나면 정점은 픽셀 몇 개로 흩어집니다. 더는 정점으로 남지 않습니다. 정점을 정점인 채로 건지려면 래스터화 앞에서 빼내야 합니다.
래스터화 앞의 갈림길
트랜스폼 피드백은 정점을 다루는 마지막 셰이더와 래스터화 사이에 붙습니다. 지오메트리 셰이더를 쓰면 그 뒤이고, 안 쓰면 정점 셰이더 바로 뒤입니다.
흐름은 거기서 둘로 갈립니다. 한 갈래는 래스터화로 가서 화면에 칠해집니다. 다른 갈래는 같은 정점을 메모리에 적습니다.
버퍼는 GPU 메모리에 잡아 둔 연속된 칸입니다. 트랜스폼 피드백은 정점을 이 버퍼에 적습니다. 적힌 버퍼는 다음 그리기 때 파이프라인 맨 앞 단계가 다시 읽어 갑니다.
flowchart TD
A["정점 읽기"] --> B["정점 셰이더"]
B --> C["지오메트리 셰이더 · 비워 둘 수 있다"]
C --> TF["트랜스폼 피드백"]
C --> R["래스터화"]
R --> P["픽셀을 칠하는 단계들 · 화면"]
TF --> M[("버퍼")]
M -.->|다음 그리기의 입력| A
그림의 점선이 이름의 뒷말입니다. 피드백은 되먹임이라는 뜻입니다. 변환한 결과를 파이프라인 입구로 되먹인다고 해서 트랜스폼 피드백입니다.
앞의 요리에 맞춰 보면, 소스를 끓이는 일이 셰이더의 계산이고 접시가 화면입니다. 따로 떠 둔 한 국자가 버퍼입니다.
이 갈래가 없으면 정점 셰이더의 결과는 화면에 칠해지고 사라집니다. 다음 그리기에서 이어 쓰려면 같은 계산을 GPU 에서 한 번 더 해야 합니다. 아니면 계산을 CPU(Central Processing Unit, 중앙 처리 장치)에서 하고 결과를 GPU 로 올려 보내야 합니다. CPU 는 그리기를 시키는 프로그램이 도는 주 처리 장치입니다.
Direct3D 는 OpenGL 과 다른 그래픽 API 입니다. Direct3D 는 같은 기능을 스트림 출력이라고 부릅니다.
무엇을 적을지 고르는 선언
정점 셰이더가 내놓는 값은 여럿입니다. 위치와 색이 있습니다. 개발자가 정한 값도 있습니다. 셰이더가 다음 단계로 넘기는 이런 출력 값이 varying 입니다.
트랜스폼 피드백은 그중 무엇을 버퍼에 적을지 미리 알아야 합니다. 개발자는 적을 varying 의 이름 목록을 넘겨 둡니다. 이 목록을 넘기는 호출이 glTransformFeedbackVaryings 입니다.
셰이더 여러 개를 이어 붙여 하나로 묶은 것이 셰이더 프로그램입니다. 셰이더는 이렇게 묶여야 쓸 수 있습니다.
셰이더를 이어 붙이는 단계가 링크입니다. 적을 목록은 링크 때 정해집니다. 그래서 이름 목록은 링크보다 먼저 넘깁니다.
적는 방식은 둘 가운데 하나를 고릅니다.
| 방식 | 적는 모양 |
|---|---|
한 버퍼에 섞어 적기 · GL_INTERLEAVED_ATTRIBS |
정점 하나의 값들을 한 버퍼에 이어 붙입니다. 첫 정점의 위치와 속도, 다음 정점의 위치와 속도가 차례로 놓입니다 |
버퍼마다 나눠 적기 · GL_SEPARATE_ATTRIBS |
varying 하나마다 버퍼를 하나씩 씁니다. 위치는 한 버퍼에, 속도는 다른 버퍼에 쌓입니다 |
버퍼 크기는 적을 값에서 셈합니다. 연기 입자 하나를 정점 하나로 다룹니다. 적을 값은 위치와 속도라고 합시다. 둘 다 실수 셋입니다. 실수 하나가 4바이트이니 정점 하나는 24바이트입니다. 입자 만 개면 버퍼를 240,000바이트 이상 잡아야 합니다.
기록을 켜고 그리기
CPU 쪽 프로그램은 네 단계를 차례로 호출합니다. 버퍼 묶기, 기록 켜기, 그리기, 기록 끄기입니다. 아래는 점 프리미티브 만 개를 그리면서 그 결과를 받아 적는 호출입니다.
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER,
0, outBuf);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 10000);
glEndTransformFeedback();
첫 호출은 결과를 적을 버퍼 outBuf 를 0번 연결 지점에 묶습니다. 버퍼를 여럿 쓰면 1번, 2번 연결 지점에 차례로 묶습니다.
glBeginTransformFeedback 과 glEndTransformFeedback 사이가 기록 구간입니다. 이 구간에서 부른 그리기의 정점이 버퍼에 적힙니다. 위 예에서는 glDrawArrays 가 점 프리미티브 만 개를 그립니다. 그 만 개의 결과가 outBuf 에 쌓입니다.
프리미티브 단위의 기록
glBeginTransformFeedback 에 넘기는 값은 적을 프리미티브의 종류입니다. 점·선분·삼각형 셋 중 하나를 고릅니다. 위 예의 GL_POINTS 는 점을 고른 것입니다.
트랜스폼 피드백은 정점을 낱개로 적지 않고 프리미티브 단위로 적습니다. 삼각형이면 정점 셋이 한 묶음으로 적힙니다. 묶음이 버퍼에 다 안 들어가면 그 삼각형은 빠집니다.
GPU 는 정점 수천 개를 동시에 계산합니다. 그래도 버퍼에는 프리미티브가 들어온 순서대로 적힙니다. 덕분에 다음 그리기에서 읽을 때 프리미티브 순서가 흐트러지지 않습니다.
삼각형 스트립은 이웃한 삼각형이 정점을 나눠 쓰게 해서 정점을 아끼는 그리기 방식입니다. 정점 넷으로 삼각형 둘을 만듭니다. 스트립으로 그린 삼각형도 버퍼에 적힐 때는 풀려서 삼각형마다 정점 셋이 따로 적힙니다. 정점 넷이 버퍼에서는 여섯이 됩니다. 그래서 스트립으로 그릴 때는 풀린 정점 수로 버퍼 크기를 셉니다.
화면에 칠하지 않고 계산만 받기
래스터화 쪽 갈래는 닫을 수 있습니다. glEnable(GL_RASTERIZER_DISCARD) 를 부르면 래스터화 단계가 프리미티브를 모두 버립니다. 화면에는 아무것도 칠해지지 않고 버퍼에만 결과가 쌓입니다.
이렇게 하면 GPU 가 계산기처럼 쓰입니다. 정점을 넣으면 셰이더가 계산합니다. 결과는 버퍼로 나옵니다. 그림이 아닌 계산에 GPU 를 쓰는 일이 GPGPU(General-Purpose computing on GPU, 범용 GPU 계산)입니다. 트랜스폼 피드백을 쓰면 파이프라인 안에서 이 일을 할 수 있습니다.
버퍼가 모자랄 때
버퍼 크기는 그리기 전에 정합니다. 그런데 지오메트리 셰이더는 프리미티브 개수를 바꿉니다. 프리미티브가 몇 개 나올지는 그리기가 끝나야 압니다.
버퍼가 차면 그 뒤에 오는 프리미티브는 적히지 않고 버려집니다. 오류가 나지 않아서 코드만 봐서는 알아채기 어렵습니다.
그래서 GPU 에 개수를 세어 달라고 맡길 수 있습니다. 이렇게 맡겨 두는 장치가 쿼리 객체입니다. 기록 구간을 쿼리로 감싸 두면 끝난 뒤에 센 값을 읽어 올 수 있습니다.
세는 값은 둘입니다. 버퍼에 적힌 프리미티브 수는 GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN 쿼리로 셉니다. 셰이더가 만들어 낸 프리미티브 수는 GL_PRIMITIVES_GENERATED 쿼리로 셉니다. 두 수가 다르면 버퍼가 모자랐던 것입니다.
개수를 묻지 않고 다시 그리기
적어 둔 버퍼를 다시 그리려면 정점이 몇 개인지 넘겨야 합니다. 지오메트리 셰이더가 개수를 바꾸거나 버퍼가 넘치면, 몇 개가 적혔는지는 GPU 만 압니다. CPU 가 쿼리로 읽어 올 수는 있지만, 그러려면 GPU 가 일을 끝낼 때까지 기다려야 합니다.
평소 CPU 는 그리기 명령을 미리 쌓아 둡니다. GPU 는 쌓인 명령을 뒤따라 처리합니다. CPU 가 개수를 받으려고 멈추면 새 명령이 안 쌓입니다. 곧 GPU 도 할 일이 떨어져 둘 다 쉬게 됩니다.
이 기다림을 없애려고 트랜스폼 피드백 객체가 있습니다. 어느 버퍼에 무엇을 몇 개 적었는지를 GPU 쪽에 담아 두는 객체입니다. glDrawTransformFeedback 에 이 객체를 넘기면 GPU 가 담아 둔 개수를 스스로 읽어 그만큼 그립니다.
프레임마다 이어 가는 입자
화면 한 장이 프레임입니다. 움직이는 화면은 프레임을 1초에 수십 장씩 새로 그려서 만듭니다.
연기나 불티처럼 작은 입자 수천 개를 그리는 기법이 파티클 시스템입니다. 입자마다 위치와 속도가 있습니다. 프레임을 그릴 때마다 위치를 조금씩 옮깁니다.
입자 위치를 GPU 에서 옮기려면 지난 프레임의 위치가 필요합니다. 트랜스폼 피드백이 그 위치를 버퍼에 남겨 줍니다.
버퍼는 둘을 번갈아 씁니다. 한 프레임은 버퍼 A 를 읽어 새 위치를 버퍼 B 에 적습니다. 다음 프레임은 B 를 읽어 A 에 적습니다. 읽는 버퍼에 곧바로 적으면 아직 안 읽은 값을 덮어쓰기 때문입니다.
지오메트리 셰이더를 함께 쓰면 입자 수도 바뀝니다. 수명이 다한 입자는 내보내지 않아 없앱니다. 새 입자는 더 내보내 만듭니다. 입자가 생기고 사라지는 일까지 GPU 안에서 끝납니다.
한 번 한 계산을 여러 번 쓰기
같은 장면을 한 프레임 안에 여러 번 그리는 경우가 있습니다. 그림자를 구하려고 한 번, 화면에 칠하려고 또 한 번 그리는 식입니다. 이 한 번 한 번이 렌더 패스입니다.
캐릭터는 뼈대가 움직이면 몸의 정점이 따라 움직입니다. 이 계산이 스키닝입니다. 정점이 많으면 무겁습니다. 트랜스폼 피드백으로 스키닝 결과를 한 번 적어 두면 뒤따르는 렌더 패스들은 그 버퍼를 읽기만 합니다.
컴퓨트 셰이더와 나누는 일
컴퓨트 셰이더는 그래픽스 파이프라인 밖에서 도는 셰이더입니다. 버퍼의 아무 칸이나 읽고 씁니다. 트랜스폼 피드백으로 하던 계산은 대개 컴퓨트 셰이더로도 할 수 있습니다.
둘은 제약에서 갈립니다. 트랜스폼 피드백은 프리미티브 단위로, 들어온 순서대로만 적습니다. 컴퓨트 셰이더에는 그런 제약이 없는 대신 순서가 필요하면 개발자가 직접 맞춥니다.
프리미티브를 들어온 순서대로 담기만 하면 되는 일에는 트랜스폼 피드백을 씁니다. 버퍼의 아무 칸에나 써야 하는 일에는 컴퓨트 셰이더를 씁니다.
Vulkan 도 그래픽 API 입니다. Vulkan 은 이 기능을 기본에 넣지 않고 확장으로만 둡니다. 그래서 Vulkan 에서는 같은 계산을 대개 컴퓨트 셰이더로 합니다.
관련 항목
트랜스폼 피드백 앞뒤에 놓이는 처리 단계
그래픽스 파이프라인 · 정점 셰이더 · 테셀레이션 · 지오메트리 셰이더 · 래스터화 · 프래그먼트 셰이더
트랜스폼 피드백이 적어 넣는 버퍼와 값
버퍼 · 정점 버퍼 · 버퍼 객체 · varying · 정점 속성 · 트랜스폼 피드백 객체
트랜스폼 피드백이 묶어 적는 도형 데이터
정점 · 프리미티브 · 삼각형 스트립 · 삼각형 리스트 · 픽셀
트랜스폼 피드백을 켜고 세고 다시 그리는 호출
glTransformFeedbackVaryings · glBeginTransformFeedback · glDrawTransformFeedback · 쿼리 객체 · 래스터라이저 폐기
트랜스폼 피드백으로 푸는 계산
파티클 시스템 · 스키닝 · GPGPU · 물리 시뮬레이션 · 핑퐁 버퍼 · 렌더 패스 · 프레임
트랜스폼 피드백과 같은 일을 하는 다른 수단
스트림 출력 · 컴퓨트 셰이더 · 메시 셰이더 · 셰이더 스토리지 버퍼
트랜스폼 피드백이 속하는 그래픽 API
OpenGL · OpenGL ES · WebGL · Direct3D · Vulkan
트랜스폼 피드백을 돌리는 장치와 프로그램
다른 이름: transform feedback · Transform Feedback · XFB · 변환 피드백