17

Part VI · Beyond Chapter 17 of 18 Build 2025.31550

다른 환경으로 가져가기

“같은 추상화가 Unity Compute Shader / Niagara / WebGPU / Houdini VEX에서 어떤 이름으로 다시 나타나는지 정리한다.”

이 챕터에서 정복할 CG 개념

왜 이게 중요한가

POP을 통해 익힌 것은 TouchDesigner의 노드 그래프 그 자체가 아니라 GPU 그래픽스 파이프라인의 데이터 모델이다. 따라서 POP을 떠나도 같은 모델은 다른 이름으로 그대로 다시 나타난다. Unity의 ComputeShader.Dispatch()는 GLSL POP의 dispatch 모드이고, WebGPU의 @compute @workgroup_size는 GLSL POP의 Work Group Size 파라미터이며, Houdini VEX의 @PTDIn_P()다. 이 챕터는 같은 추상화의 환경별 사전을 만든다. 새 환경에 처음 들어갈 때 검색해야 하는 키워드 목록이라고 보면 된다.

POP에서의 노출 지점

다음 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)

WebGPU / WGSL (https://gpuweb.github.io/gpuweb/, https://gpuweb.github.io/gpuweb/wgsl/, https://webgpufundamentals.org/webgpu/lessons/webgpu-compute-shaders.html)

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)

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)

손작업 (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)

  1. TouchDesigner POP과 Houdini POP은 어떻게 다른가? 둘을 한 문장으로 구분해서 다른 사람에게 설명할 수 있는가?
  2. TDIndex()에 대응하는 변수가 HLSL·WGSL·VEX에서 각각 무엇인가? 그 중 어느 것이 GLSL POP과 가장 가까운가?
  3. POP의 Work Group Size 파라미터가 Unity Compute Shader에서 사라지지 않는다. 어디로 갔는가?
  4. Niagara의 "Particle Attribute"는 POP의 어떤 attribute class에 가장 가까운가? Emitter 어트리뷰트는?
  5. WebGPU에서 ping-pong을 구현하려면 매 프레임 무엇을 swap해야 하는가? POP의 Feedback POP은 그것을 어떻게 감추는가?

연결 고리

이 챕터의 한 줄 명제

POP에서 손에 익은 dispatch·SSBO·attribute·ping-pong·instancing은 환경이 바뀌어도 이름만 바뀐다. 표 한 장이 그 사전이다.