이 핸드북의 핵심 명제는 "POP은 GPU 그래픽스 파이프라인의 데이터 모델을 노드 그래프로 노출시킨 학습 환경"이다. 이 챕터는 그 명제를 GPU 메모리 모델의 차원에서 다시 진술한다. 텍스처와 vertex buffer는 같은 바이트 풀에 다른 해석을 씌운 것이다. TOP의 픽셀과 POP의 attribute는 메모리 안에서 동일한 자료다. TouchDesigner POP과 Houdini POP(Particle Operators)은 약자만 같을 뿐 무관하다 — 이 책의 POP은 항상 Point Operator를 가리킨다.
이 챕터에서 정복할 CG 개념
- GPU의 typed memory 모델. 같은 바이트 풀이 texture, vertex buffer, SSBO 세 가지 인터페이스로 해석될 수 있다는 사실.
- 2D 인덱싱 + 샘플러 + filter vs 1D 인덱싱 + attribute fetch의 대비.
- Vertex Texture Displacement — vertex stage에서 texture를 샘플링해 P를 변위시키는 고전 기법.
- "데이터 도메인의 재해석"이 GPU에서 비용 없이 가능한 경우와 그렇지 않은 경우의 구분.
왜 이게 중요한가
셰이더 학습자가 가장 늦게 깨닫는 사실은 "texture는 메모리다"라는 단순한 사실이다. fragment shader에서 texture(...)를 호출하면 sampler가 픽셀을 읽지만, 같은 메모리 영역을 vertex shader가 vertex attribute로 읽어도 GPU는 불평하지 않는다. 차이는 binding의 종류 — 즉 어떤 인터페이스로 메모리를 잡는가 — 뿐이다. WebGPU에서 storage buffer라는 단일 추상화가 이 사실을 표면화한다. POP은 그 추상화를 노드 그래프 위에서 직접 보여준다. TOP to POP POP은 GPU 메모리상의 픽셀 RGBA를 attribute로 재해석하는 노드다. 같은 메모리를 다르게 부르는 것 — 이것이 모든 GPGPU의 출발점이다.
POP에서의 노출 지점
이 챕터의 주역은 세 노드다.
- TOP to POP (Bridge POP): TOP의 픽셀을 POP 포인트로 변환한다. RGBA를 color로 받을 수도, position+active로 받을 수도, depth/height/custom attribute로 받을 수도 있다. Wiki 인벤토리상 verbatim 설명: "Converts a TOP's pixels into POP points; interpret RGBA as color, position+active, position, depth, height, or custom attributes."
- Lookup Texture POP (Attribute POP): 입력 POP의 U/V 0–1 attribute를 인덱스로 사용해 TOP의 RGBA를 샘플링하고, 그 결과를 POP attribute에 기록한다. 즉 텍스처를 lookup table로 쓰는 것.
- GLSL POP / GLSL Advanced POP (Programmable POP): Samplers 페이지에 TOP을 바인딩하면 compute shader 안에서
texture(...)호출이 가능해진다. 같은 텍스처를 fragment에서 읽던 함수 그대로 compute에서 읽는다.
핵심 차이는 TOP to POP은 픽셀 그리드를 포인트 그리드로 1:1 매핑하는 데 비해, Lookup Texture POP은 임의의 U/V 좌표에서 샘플링한다는 점이다. 전자는 "픽셀 = 포인트"의 가장 직접적 형태이고, 후자는 "텍스처 = 임의 함수의 표"라는 더 일반적 관점이다.
이론
GPU 메모리에는 본질적으로 세 종류의 "데이터 객체"가 존재하지 않는다. 존재하는 것은 typed allocation 하나뿐이다. 그 위에 어떤 view를 씌우느냐가 모든 차이를 만든다.
GPU device memory
|
+-- typed allocation (raw bytes + element format)
|
+-- 2D 인덱싱 + filter + sampler → Texture (TOP)
+-- 1D 인덱싱 + attribute fetch → Vertex Buffer (POP attribute)
+-- 1D 인덱싱 + atomic/coherent → SSBO (compute shader)
같은 1024×1024 RGBA8 영역을 fragment shader에서는 1M개의 픽셀로 보고, compute shader에서는 1M개의 float4 값으로 본다. 픽셀 (x,y)와 1D 인덱스 y*1024 + x의 변환은 인덱싱 산수일 뿐 데이터 복사가 아니다.
여기서 중요한 것: TOP to POP은 이론적으로는 메모리 복사 없이 reinterpret만으로 구현 가능하다. 그러나 wiki는 내부 구현이 reference인지 copy인지 명시하지 않는다. 다른 POP들의 "References to unmodified input attributes" 정책이 TOP→POP 경로에도 그대로 적용되는지는 wiki 미확정 — 실험 필요.
Heightmap displacement는 이 동등성의 가장 단순한 응용이다. 한 채널(R)을 높이로 해석해 정점의 y에 더한다. fragment shader 학습자가 "노이즈 텍스처를 만들어 화면에 그린다"고 부르는 작업이, vertex shader 측에서는 "노이즈 텍스처를 정점 변위에 쓴다"가 된다. 같은 텍스처, 다른 인덱싱.
데이터 흐름 다이어그램
GPU Device Memory
+--------------------------------------------------+
| |
| [Noise TOP allocation] |
| 1024 x 1024 x RGBA8 |
| |
+-----+--------------------+------------------------+
| |
| as 2D sampler | as 1D attribute source
v v
fragment shader: TOP to POP POP:
texture(s, uv).r R channel → P.y per point
| |
v v
pixel color vertex P attribute
|
v
vertex shader
(MVP * P, etc.)
|
v
fragment shader
(lit per-pixel)
오른쪽 경로는 "TOP의 픽셀 데이터가 POP attribute로 들어가서, 그 POP을 Geometry COMP에 넣고 Render TOP으로 그리면, 결과적으로 픽셀이 다시 픽셀이 된다"는 왕복을 보여준다. 라운드트립 사이에 데이터의 의미는 한 번 바뀌었다 — 색에서 형태로.
손작업 (Hands-on)
시작 파일
- Noise TOP1 (Type: Sparse, Period: 4, Resolution: 256 x 256, Monochrome On)
- Grid POP1 (Rows: 256, Columns: 256, Connectivity: Triangles)
- TOP to POP1 (Input: Noise TOP1, Convert Mode: Position + Active 또는 Height — 아래 단계에서 설명)
- Normal POP1 (Type: Vertex)
- Geometry COMP1 (containing POPs)
- Render Simple TOP1
연결: Grid POP1 → TOP to POP1 (두 번째 입력으로 Noise TOP1 또는 Samplers/Convert Mode 사용에 따라 별도) → Normal POP1 → Geometry COMP1 → Render Simple TOP1
wiki 미확정 — 실험 필요: TOP to POP이 입력 POP을 받아 그 P attribute의 y에 TOP의 R 채널을 합치는 "displace 모드"를 가지는지, 아니면 항상 TOP 단일 입력으로부터 새로운 포인트 집합을 생성하는지는 wiki의 한 줄 요약("Converts a TOP's pixels into POP points; interpret RGBA as color, position+active, position, depth, height, or custom attributes")으로는 정확히 갈리지 않는다. 인벤토리에 의하면 RGBA를 "height"로 해석하는 모드가 명시적으로 존재한다. 즉 TOP만 받아 픽셀 그리드를 포인트 그리드로 변환하면서 R 채널을 P.y로 쓰는 모드가 1차 후보다. 이 경우 Grid POP은 불필요하고, TOP to POP 한 노드가 256×256 포인트를 직접 생성한다.
1단계: 256×256 픽셀을 256×256 포인트로
- Noise TOP1을 만들고 Monochrome을 On으로 설정. R 채널이 높이값으로 쓰일 것이다.
- TOP to POP1을 Noise TOP1에 연결. Convert Mode를 "Height" 또는 등가의 모드로 설정 (정확한 메뉴 라벨은 wiki 미확정 — 실험 필요, 인벤토리상 "height"가 모드 중 하나로 언급됨).
- 보게 될 것: 256×256 = 65,536개의 포인트가 grid 모양으로 배치된다. 각 포인트의 P.x, P.z는 픽셀 좌표를 0–1로 정규화한 값에 비례하고, P.y는 노이즈 텍스처의 R 채널 값이다.
2단계: 노멀 계산과 렌더
- Normal POP1을 연결. Type을 Vertex로. heightmap이 변하면 노멀도 변해야 하므로 이 단계가 필수다.
- POP 노드들을 Geometry COMP1 안에 넣고 Render Simple TOP1로 본다.
- 보게 될 것: 노이즈가 만들어낸 산악 지형 같은 표면. Phong shading이 적용되면 노멀이 텍스처에 따라 자연스럽게 변화하는 모습이 보인다.
3단계: Noise TOP을 시간 함수로
- Noise TOP1의 Translate를
absTime.seconds * 0.1로 묶는다 (Z translate 권장). - 보게 될 것: 표면이 움직인다. 노이즈 텍스처의 변화가 그대로 정점 변위로 흘러간다. 텍스처 한 장이 매 프레임 다시 샘플링되어 vertex buffer에 흘러들어가는 것이다.
직관과 어긋나는 부분: "텍스처를 정점에 쓴다"는 말이 어색하게 들리는 이유는 학습 순서가 fragment shader 먼저이기 때문이다. 메모리 모델 차원에서 보면 vertex가 texture를 읽는 것은 fragment가 읽는 것과 비용 구조도 거의 같다 (modern hardware의 vertex texture fetch unit이 fragment의 그것과 통합되어 있다). RTR4 §3, §6 참조.
노드 ↔︎ GLSL 매핑
같은 결과를 두 방식으로 만든다. 노드 버전에서는 TOP to POP이 "height" 해석을 내장 처리한다. GLSL 버전에서는 그 해석을 우리가 직접 쓴다.
노드 버전 (재진술)
Noise TOP1 (256x256, monochrome)
│
▼
TOP to POP1 (Convert Mode: Height-equivalent)
│
▼
Normal POP1 (Vertex)
│
▼
Geometry COMP1 → Render Simple TOP1
GLSL POP 버전
Grid POP에서 시작해 정점을 만들고, GLSL POP에서 Samplers 페이지로 Noise TOP을 바인딩한 뒤 R 채널을 P.y로 쓴다.
노드 구성
- Grid POP1 (Rows: 256, Columns: 256, Size: 2 x 2)
- Noise TOP1 (256 x 256, monochrome)
- GLSL POP1 (Attribute Class: Point, Output Attributes: P, Number of Threads: Auto)
- Samplers 페이지:
sampler0name = sNoise,sampler0top = ../Noise TOP1,sampler0filter = linear
- Samplers 페이지:
- Normal POP1 (Type: Vertex)
- Geometry COMP1 → Render Simple TOP1
Compute Shader (DAT)
// GLSL POP, Attribute Class = Point, Output Attributes = P
// Samplers page: sNoise = Noise TOP1, linear filter, extend = hold
uniform sampler2D sNoise;
uniform float uAmplitude; // Vectors page: float, uAmplitude, value 0.5
void main() {
const uint id = TDIndex();
if (id >= TDNumElements()) return;
// Grid POP1의 원본 P를 읽는다. P.x, P.z는 평면 위 0~1 정규화 좌표로 매핑된다.
vec3 p = TDIn_P();
// P.x, P.z를 0~1 UV로 변환한다. Grid 크기가 2x2이므로 절반 더해 정규화.
vec2 uv = p.xz * 0.5 + 0.5;
// Noise TOP의 R 채널을 높이로 해석.
float h = texture(sNoise, uv).r;
p.y = h * uAmplitude;
// 출력. P는 Output Attributes에 명시되어 있어야 컴파일이 통과한다.
P[id] = p;
}
이 코드가 GPU에서 실제로 무엇을 하고 있는가: 65,536개의 thread가 각각 하나의 포인트를 담당한다. 각 thread는 자기 점의 P.x, P.z를 UV로 변환해 sampler 인터페이스를 통해 Noise TOP의 메모리를 읽고, 그 값을 자기 점의 P.y에 기록한다. 동일한 GPU 메모리 영역(noise texture allocation)이 65,536개의 thread에서 동시 read-only로 접근된다. 출력 SSBO인 P attribute buffer에는 thread당 한 번의 write가 발생한다. fragment shader가 같은 텍스처를 64만 픽셀에서 읽는 작업과 본질적으로 같은 메모리 트래픽이다.
두 버전의 차이
- 노드 버전은 TOP to POP이 "픽셀 좌표 → 포인트 좌표 + 높이" 변환을 내장 처리한다. UV 계산이 노드 내부에 있다.
- GLSL 버전은 UV 계산을 사용자가 명시한다. 그 대가로 임의의 텍스처 좌표 매핑(예: 회전, 도메인 워핑)을 자유롭게 끼울 수 있다.
두 결과는 같은 메모리(Noise TOP의 R 채널)를 같은 의미(높이)로 읽는다. 차이는 "변환 로직이 노드에 캡슐화되어 있는가, 셰이더에 노출되어 있는가"뿐이다.
확인 질문 (Self-check)
- 같은 1024×1024 RGBA8 영역이 fragment shader, vertex shader, compute shader에서 각각 어떤 인터페이스로 접근되는가? 셋은 같은 메모리를 가리키는가?
texture(sNoise, uv).r을 vertex 단계에서 호출하는 것과 fragment 단계에서 호출하는 것의 비용 차이는 무엇에서 발생하는가? 발생하지 않는다면 왜인가?- TOP to POP이 "Position + Active" 모드에서 RGB를 P로, A를 active flag로 해석한다고 가정하자. 이때 RGBA 채널 순서를 BGRA로 바꾸려면 어디서 조작해야 하는가?
- Heightmap displacement 후 Normal POP을 다시 돌려야 하는 이유는? Normal을 다시 돌리지 않고 GLSL POP 안에서 노멀을 추정하려면 어떤 데이터가 필요한가?
- WebGPU의 storage buffer 추상화에서 위와 동등한 표현을 적으면 어떤 모양이 되는가? 텍스처/버퍼의 경계가 거기서는 어떻게 보이는가?
연결 고리
- LearnOpenGL — "Framebuffers" (https://learnopengl.com/Advanced-OpenGL/Framebuffers). Render-to-texture를 다루며 "텍스처는 buffer다"라는 명제를 가장 명료하게 보여준다.
- GPU Gems 2 Ch.18 "Using Vertex Texture Displacement for Realistic Water Rendering" (https://developer.nvidia.com/gpugems/gpugems2). 이 챕터의 손작업이 그대로 책의 사례다.
- Real-Time Rendering 4ed §6 "Texturing". 텍스처 포맷, 샘플러, 변위 매핑의 정식 정리.
- Real-Time Rendering 4ed §3 "The Graphics Processing Unit". vertex texture fetch unit이 fragment와 통합된 modern GPU 아키텍처.
- WebGPU Fundamentals — "Storage Buffers" (https://webgpufundamentals.org/webgpu/lessons/webgpu-storage-buffers.html). "Buffer는 그저 typed memory"라는 관점의 가장 깨끗한 표현.
- Khronos OpenGL Wiki — "Buffer Object" (https://wikis.khronos.org/opengl/Buffer_Object). VBO/EBO/UBO/SSBO가 모두 같은 객체임을 spec 차원에서 확인.
이 챕터의 한 줄 명제
GPU 위에서 텍스처와 vertex buffer는 같은 메모리에 씌운 다른 view다. TOP to POP은 그 view 전환을 노드 한 개로 노출한 학습 장치이며, heightmap displacement는 그 전환이 일어나는 가장 단순한 사례다.