이 챕터에서 정복할 CG 개념
- POP에서 손에 익힌 추상화(attribute, dispatch, workgroup, SSBO, multi-pass, instancing)가 다른 환경에서 어떤 이름으로 다시 등장하는지 1:1로 매핑한다.
- 같은 단어가 환경마다 다른 것을 가리키는 사례(POP, vertex, attribute, particle system)를 명시적으로 분리한다.
- Unity Compute Shader / Unreal Niagara / WebGPU / Houdini VEX의 첫 한 시간을 POP 경험 위에서 어떻게 시작할 것인지의 체크리스트를 갖춘다.
왜 이게 중요한가
POP을 통해 익힌 것은 TouchDesigner의 노드 그래프 그 자체가 아니라 GPU 그래픽스 파이프라인의 데이터 모델이다. 따라서 POP을 떠나도 같은 모델은 다른 이름으로 그대로 다시 나타난다. Unity의 ComputeShader.Dispatch()는 GLSL POP의 dispatch 모드이고, WebGPU의 @compute @workgroup_size는 GLSL POP의 Work Group Size 파라미터이며, Houdini VEX의 @P는 TDIn_P()다. 이 챕터는 같은 추상화의 환경별 사전을 만든다. 새 환경에 처음 들어갈 때 검색해야 하는 키워드 목록이라고 보면 된다.
POP에서의 노출 지점
다음 POP 기능들이 본 챕터의 매핑 행에 그대로 대응한다.
- Point/Vertex/Primitive 분리 → Attribute POP, Normal POP, Attribute Convert POP.
- Custom 어트리뷰트 → Attribute POP의 New Attribute 블록, GLSL POP/Advanced POP의 Create Attributes 페이지.
- Compute dispatch → GLSL POP / GLSL Advanced POP의 Number of Threads, Work Group Size, Dispatch Size 파라미터.
- 인덱스/스레드 식별 →
TDIndex(),TDNumElements(). - SSBO → 모든 GLSL POP 출력 어트리뷰트 (
AttribName[],oTDPoint_AttribName[]). - Sampler → GLSL POP Samplers 페이지.
- Multi-pass → GLSL POP의
npasses,prevpassoutput. - Feedback / ping-pong → Feedback POP, Cache POP.
- Instancing → Geometry COMP의 POP 입력을 통한 instance 어트리뷰트 공급.
- 이웃 질의 → Neighbor POP, Proximity POP.
이론
다이어그램 #6 — 핸드북 개념의 환경 간 매핑
같은 개념이 어떤 이름으로 다시 나타나는지의 정사각형 표.
| 핸드북 개념 (TD POP) | OpenGL / Vulkan | Unity Compute / DOTS | Unreal Niagara | Houdini VEX / SOP | WebGPU / WGSL |
|---|---|---|---|---|---|
| Point attribute | per-vertex in (이름 충돌: GL "vertex" = POP "point") |
ComputeBuffer<T> 한 요소, 또는 DOTS IComponentData |
Particle Attribute (Position, Velocity, …) — Particle 단위 | Point attribute @P, @N 등 (point class) |
array<T> in storage buffer, 한 요소 |
| Vertex attribute | 인덱싱된 vertex의 per-corner 데이터 (현실에선 별도 attribute가 거의 없음) | per-vertex stream / Mesh SetVertexBufferData |
(Niagara는 Mesh Renderer의 vertex 채널을 직접 다루지 않음) | Vertex attribute @vtxnum, vertex() 함수 |
Vertex buffer attribute (@location(n)) |
| Primitive attribute | per-primitive 데이터는 표준 없음 (flat qualifier 또는 SSBO로 우회) |
별도 ComputeBuffer 또는 [Material]Property 블록 |
Particle = primitive로 간주 | Primitive attribute (prim class), primintrinsic() |
Storage buffer, primitive id로 인덱스 |
| Custom per-element attribute | layout(location=N) in T name (vertex) 또는 SSBO |
ComputeBuffer.SetData(...), HLSL StructuredBuffer<T> |
Custom Attribute (Module → Set Particle Attribute) | i@myAttr, f@myAttr, v@myAttr (VEX 자동 선언) |
@group(0) @binding(N) var<storage, read> name: array<T> |
| Compute shader dispatch | glDispatchCompute(x,y,z) |
ComputeShader.Dispatch(kernelIndex, x, y, z) |
Emitter GPU Sim + Spawn/Update 단계 (내부적으로 indirect dispatch) |
(없음 — VEX는 CPU/SIMD 또는 OpenCL wrangle) | pass.dispatchWorkgroups(x, y, z) |
| Workgroup / thread | GLSL layout(local_size_x=...) in; / gl_LocalInvocationID |
HLSL [numthreads(x,y,z)], SV_GroupThreadID |
(Niagara는 사용자에게 numthreads를 노출하지 않음) | OpenCL Wrangle SOP의 workgroup (실험적) | WGSL @workgroup_size(x,y,z), @builtin(local_invocation_id) |
| Element index (TDIndex) | gl_GlobalInvocationID.x |
SV_DispatchThreadID.x (= id.x in id : SV_DispatchThreadID) |
ExecutionIndex (Particle Update 시점 자동) |
VEX의 암묵 인덱스 @elemnum / @ptnum |
@builtin(global_invocation_id) gid : vec3<u32> |
| SSBO / read-write buffer | layout(std430, binding=N) buffer ... |
HLSL RWStructuredBuffer<T> |
DataInterface (DI_Skeletal/Static Mesh/Grid…) | DetailAttribWrangle + 자체 attribute, 또는 OpenCL __global 버퍼 |
var<storage, read_write> name: array<T> |
| Sampler (texture binding) | uniform sampler2D tex; texture(tex, uv) |
HLSL Texture2D tex; SamplerState s; tex.Sample(s, uv) |
Sample Texture Module / Sample Skeletal Mesh | VEX texture("path", u, v) |
var t: texture_2d<f32>; var s: sampler; textureSample(t, s, uv) |
| Multi-pass | 같은 dispatch를 N번 호출 + barrier, 또는 여러 셰이더 체이닝 | C# 측에서 Dispatch를 N회 호출, 사이에 GraphicsFence 가능 |
Niagara Stage(Simulation Stage) — pass 단위 명시 가능 (GPU sim) | SOP Solver / For-loop SOP / iterate Wrangle | 같은 encoder에서 dispatchWorkgroups를 N회, 사이에 barrier() 또는 별도 pass |
| Feedback / ping-pong | 두 SSBO(또는 두 텍스처) 바인딩을 매 프레임 swap | ComputeBuffer 두 개, A→B, 다음 프레임 B→A |
Niagara는 Particle 상태가 프레임 간 자동 보존되므로 사용자가 ping-pong을 직접 작성하지 않음 | Solver SOP — 이전 프레임 출력이 자동으로 다음 프레임 입력 | 두 storage buffer 핑퐁; 매 프레임 bind group을 swap |
| Instancing | glDrawArraysInstanced(...) + gl_InstanceID |
Graphics.RenderMeshInstanced / DrawMeshInstancedProcedural / Mesh Renderer |
Niagara Mesh Renderer (한 mesh × N particle) | Copy to Points SOP, Instance attribute (@instance) |
passEncoder.draw(vertexCount, instanceCount) + @builtin(instance_index) |
| Spatial neighborhood query | 직접 구현 (uniform grid SSBO, GPU Gems 3 Ch. 32) | DOTS KDTree 라이브러리, 또는 직접 ComputeBuffer 기반 grid |
Niagara Neighbor Grid 3D Data Interface | VEX nearpoints(), pcfind(), xyzdist(), PointCloud 함수 |
직접 구현 (storage buffer 기반 grid) |
위 표의 셀은 100% 동치를 주장하지 않는다. 같은 슬롯에 들어가는 가장 비슷한 개념이라는 의미다. 다음 절에서 표의 한계를 짚는다.
매핑이 가지는 한계 — 같은 이름이 같은 의미가 아닌 사례
POP — TouchDesigner의 POP은 Point Operator이다. Houdini의 POP은 Particle Operators, 즉 Houdini 초기 입자 시스템의 약자다. Houdini POP은 현재 단종되어 DOP 네트워크 기반 입자로 대체되었다 (https://www.sidefx.com/docs/houdini/dopparticles/index.html). 같은 세 글자, 전혀 다른 것. SideFX 문서에서 "POP"을 검색하면 거의 항상 후자가 나온다. 본 핸드북에서 "POP"은 항상 TouchDesigner의 Point Operator를 가리킨다.
vertex — POP에서 vertex는 primitive의 한 모서리에 붙은 per-corner 데이터다 (Houdini의 vertex와 동일). OpenGL/LearnOpenGL/RTR4의 "vertex"는 보통 POP의 point에 해당한다. OpenGL에서 한 vertex가 여러 primitive에 공유되면 EBO(index buffer)로 그 인덱스를 명시한다. POP은 이 분리를 어트리뷰트 클래스 그 자체로 노출한다. 같은 단어가 두 가지 메모리 위치를 지칭한다. 이 핸드북에서 vertex는 POP 의미로만 쓴다.
attribute — GLSL에서 attribute (또는 in qualifier, vertex stage)는 per-vertex 입력 변수다. POP에서 attribute는 Point/Vertex/Primitive/Detail 어디든 살 수 있는 named per-element value다. 둘은 겹치지만 같지 않다. POP attribute는 GLSL에서 SSBO로 백킹된다 ("Each attribute is a Shader Storage Buffer Object (SSBO)" — Write_a_GLSL_POP).
compute shader / kernel / shader graph — OpenGL·Vulkan·D3D11+·Metal·WebGPU에서 "compute shader"는 같은 것을 가리킨다. CUDA·OpenCL은 같은 개념을 "kernel"이라 부른다. Unity Shader Graph·Unreal Material Editor·TouchDesigner GLSL 노드는 셰이더 그 자체가 아니라 셰이더를 생성하는 visual graph다. 헷갈리지 말 것.
buffer — OpenGL/WebGPU에서 buffer는 VBO·EBO·SSBO·UBO를 통칭하는 typed memory다. TouchDesigner의 일상 어휘에서 "buffer"는 흔히 render target 또는 framebuffer를 의미한다. Ch.4의 "텍스처와 vertex buffer는 같은 GPU 메모리다"라는 명제는 이 구분을 먼저 분리한 다음에만 성립한다.
particle system — Houdini에서는 DOP 네트워크 입자 솔버. Unreal에서는 Niagara(또는 구형 Cascade). TouchDesigner에서는 Particle SOP, Particle GPU TOP, 그리고 본 핸드북이 가르치는 POP 기반 입자 시뮬레이션이 모두 "particle system"으로 불릴 수 있다. 같은 표현이 네 가지 서로 다른 구현을 가리킨다.
다음 환경으로 옮길 때의 첫 한 시간 체크리스트
Unity Compute Shader (https://docs.unity3d.com/Manual/class-ComputeShader.html)
.compute파일을 만들고#pragma kernel CSMain을 선언한다.[numthreads(64, 1, 1)]한 줄은 GLSL POP의 Work Group Size와 같다.- C# 측에서
ComputeBuffer cb = new ComputeBuffer(N, sizeof(float)*3);로 SSBO를 만든다. 이는 GLSL POP에서 Output Attributes로 잡힌 P 버퍼와 같다. cs.SetBuffer(kernel, "Points", cb);→ POP에서oTDPoint_P[]가 자동 바인딩되던 것을 수동으로 하는 것에 해당한다.cs.Dispatch(kernel, Mathf.CeilToInt(N/64f), 1, 1);— 이 한 줄이 GLSL POP의 "Number of Threads: Auto"의 명시판이다.Mathf.CeilToInt로 올림하는 것이 POP에서 자동으로 일어나던 workgroup-round-up.- HLSL 측:
RWStructuredBuffer<float3> Points;그리고id.x는TDIndex().if (id.x >= N) return;관용구는 그대로 유지. - DOTS/Burst는 같은 데이터 모델을 CPU SIMD로 옮긴 것이다.
IComponentData가 어트리뷰트,IJobEntity가 dispatch.
WebGPU / WGSL (https://gpuweb.github.io/gpuweb/, https://gpuweb.github.io/gpuweb/wgsl/, https://webgpufundamentals.org/webgpu/lessons/webgpu-compute-shaders.html)
- WGSL에서 compute entry point는
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid : vec3<u32>) { ... }.@workgroup_size(64)는 GLSL POP의 Work Group Size,gid.x는TDIndex(). - 버퍼 바인딩:
@group(0) @binding(0) var<storage, read_write> points : array<vec3<f32>>;—GPUBufferBindingType.storage가 read-write SSBO,read-only-storage가 read-only. - TS/JS 측:
device.createBuffer({ size, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST })로 SSBO 생성,bindGroup으로 바인딩,passEncoder.dispatchWorkgroups(Math.ceil(N/64))로 dispatch. - WebGPU의 storage buffer 개념은 WebGPU Fundamentals의 "Storage Buffers" 강의 (https://webgpufundamentals.org/webgpu/lessons/webgpu-storage-buffers.html)에 가장 명료하게 정리되어 있다. POP의 어트리뷰트 SSBO가 그대로 옮겨진다.
Houdini VEX / SOP (https://www.sidefx.com/docs/houdini/vex/index.html, https://www.sidefx.com/docs/houdini/model/attributes.html, https://www.sidefx.com/docs/houdini/nodes/sop/attribpromote.html)
- Attribute Wrangle SOP가 GLSL POP에 가장 가깝다. Run Over를 "Points / Vertices / Primitives"로 설정하면
attrclass를 고르는 것과 같다. - VEX에서
@P는 현재 element의 P 어트리뷰트.@P.x = ...;로 직접 쓴다. POP의TDIn_P()+P[id] = ...;와 동일한 의미.@ptnum이TDIndex(). - 새 어트리뷰트는 타입 prefix로 자동 선언:
f@speed = 0.0;,v@vel = {0,0,0};,i@id = 0;. GLSL POP의 Create Attributes 페이지 한 줄짜리 버전. - 어트리뷰트 클래스 간 변환은
attribpromoteSOP. POP의 Attribute Convert POP과 1:1. - 주의: Houdini의 "POP"은 본 핸드북의 POP과 무관하다. Houdini에서 GPU 컴퓨트를 직접 쓰려면 OpenCL Wrangle SOP를 본다.
Unreal Niagara (https://dev.epicgames.com/documentation/en-us/unreal-engine/overview-of-niagara-effects-for-unreal-engine, verify exact UE-version slug at publication)
- Niagara의 단위는 Emitter — 한 종류의 particle을 만들고 갱신하는 단위. Particle Attribute(Position, Velocity, MeshOrientation, Age, …)이 핸드북의 어트리뷰트.
- Spawn → 한 번 실행되는 초기화 모듈. Update → 매 프레임 실행되는 모듈. GLSL POP의 Initialize Output Attributes(initoutputattrs)와 main 셰이더의 분리와 유사.
- GPU Sim 모드 ON: dispatch가 GPU에서 일어난다. CPU Sim 모드에서는 같은 모듈이 CPU에서 실행. 같은 모듈, 같은 attribute 모델.
- Custom 어트리뷰트는 "Set New or Existing Particle Attribute" 모듈로 추가. 어트리뷰트는 Particle, Emitter, System 세 스코프 (= Point / Detail / Detail²)에 살 수 있다.
- Simulation Stage(Stage)는 POP의 multi-pass와 가장 가까운 추상화다. 한 Emitter 안에서 여러 dispatch를 명시적으로 정렬한다.
손작업 (Hands-on)
본 챕터는 메타 챕터이므로 손작업은 의도적으로 짧다. POP의 가장 작은 컴퓨트 셰이더 한 줄을 Unity·WebGPU 두 환경의 동등 코드와 나란히 놓는다.
POP (GLSL POP, Attribute Class = Point, Output Attributes = P)
void main() {
// 1차원 dispatch — 한 스레드가 한 점을 담당
const uint id = TDIndex();
if (id >= TDNumElements()) return;
// 입력 점 위치를 0.1만큼 위로 올려서 출력에 쓴다
P[id] = TDIn_P() + vec3(0.0, 0.1, 0.0);
}
이 코드의 의미: TDIndex()로 자신의 1D 인덱스를 가져오고, TDNumElements() 초과 인덱스는 즉시 반환. TDIn_P()로 input 0의 P를 읽고, 더한 결과를 출력 SSBO P[id]에 쓴다. POP은 workgroup size·dispatch size·바인딩을 모두 파라미터에서 자동 결정한다.
Unity Compute Shader (Lift.compute)
// 1차원 dispatch — 한 스레드가 한 점을 담당
#pragma kernel CSMain
RWStructuredBuffer<float3> Points;
uint Count;
[numthreads(64, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID) {
if (id.x >= Count) return;
// 입력 점 위치를 0.1만큼 위로 올려서 출력에 쓴다
Points[id.x] = Points[id.x] + float3(0.0, 0.1, 0.0);
}
C# 측에서 cs.SetBuffer(0, "Points", buf); cs.SetInt("Count", N); cs.Dispatch(0, Mathf.CeilToInt(N/64f), 1, 1);. POP에서 자동이던 dispatch·바인딩·올림이 모두 명시적으로 드러난다.
WebGPU (WGSL)
// 1차원 dispatch — 한 스레드가 한 점을 담당
@group(0) @binding(0) var<storage, read_write> points : array<vec3<f32>>;
@group(0) @binding(1) var<uniform> count : u32;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
let id = gid.x;
if (id >= count) { return; }
// 입력 점 위치를 0.1만큼 위로 올려서 출력에 쓴다
points[id] = points[id] + vec3<f32>(0.0, 0.1, 0.0);
}
JS 측에서 passEncoder.setPipeline(...); passEncoder.setBindGroup(0, bg); passEncoder.dispatchWorkgroups(Math.ceil(N/64));. WGSL의 @workgroup_size(64), @builtin(global_invocation_id), array<vec3<f32>> 세 줄이 POP의 Work Group Size·TDIndex()·Output Attribute의 동의어다.
세 코드는 같은 일을 한다. 같은 dispatch 모델, 같은 SSBO, 같은 bounds-check. 환경이 바뀌어도 모델은 바뀌지 않는다.
노드 ↔︎ GLSL 매핑
본 챕터는 메타 챕터이므로 별도의 노드↔︎GLSL 매핑은 두지 않는다. 위 손작업 블록 자체가 매핑이다.
확인 질문 (Self-check)
- TouchDesigner POP과 Houdini POP은 어떻게 다른가? 둘을 한 문장으로 구분해서 다른 사람에게 설명할 수 있는가?
TDIndex()에 대응하는 변수가 HLSL·WGSL·VEX에서 각각 무엇인가? 그 중 어느 것이 GLSL POP과 가장 가까운가?- POP의 Work Group Size 파라미터가 Unity Compute Shader에서 사라지지 않는다. 어디로 갔는가?
- Niagara의 "Particle Attribute"는 POP의 어떤 attribute class에 가장 가까운가? Emitter 어트리뷰트는?
- WebGPU에서 ping-pong을 구현하려면 매 프레임 무엇을 swap해야 하는가? POP의 Feedback POP은 그것을 어떻게 감추는가?
연결 고리
- Unity Manual — Compute shaders (https://docs.unity3d.com/Manual/class-ComputeShader.html) — Create / SetBuffer / Dispatch / numthreads의 일차 문서.
- Unity Manual — Entities overview (DOTS/ECS, https://docs.unity3d.com/Packages/com.unity.entities@1.3/manual/index.html) — 어트리뷰트 모델의 CPU 변형.
- Unity Manual — Burst compiler (https://docs.unity3d.com/Packages/com.unity.burst@1.8/manual/index.html) — 같은 모델의 SIMD 컴파일.
- Unreal Engine — Overview of Niagara Effects (https://dev.epicgames.com/documentation/en-us/unreal-engine/overview-of-niagara-effects-for-unreal-engine) — UE 버전마다 slug가 바뀐다. 출판 시점에 정확한 경로 재확인 필요.
- WebGPU 스펙 (https://gpuweb.github.io/gpuweb/), WGSL 스펙 (https://gpuweb.github.io/gpuweb/wgsl/), WebGPU Fundamentals (https://webgpufundamentals.org/).
- Houdini 문서 — VEX (https://www.sidefx.com/docs/houdini/vex/index.html), Geometry attributes (https://www.sidefx.com/docs/houdini/model/attributes.html), Attribute Promote SOP (https://www.sidefx.com/docs/houdini/nodes/sop/attribpromote.html), Particles (https://www.sidefx.com/docs/houdini/dopparticles/index.html).
- LearnOpenGL — Compute Shaders Introduction (https://learnopengl.com/Guest-Articles/2022/Compute-Shaders/Introduction) — 환경 중립적인 dispatch 그림.
- Real-Time Rendering 4ed Ch. 3 "The Graphics Processing Unit" — SIMT / warp / wavefront의 책 쪽 용어.
이 챕터의 한 줄 명제
POP에서 손에 익은 dispatch·SSBO·attribute·ping-pong·instancing은 환경이 바뀌어도 이름만 바뀐다. 표 한 장이 그 사전이다.