15

Part V · Pipeline Chapter 15 of 18 Build 2025.31550

Instancing

“한 mesh와 백만 개 attribute 행이 있으면 백만 개 draw call이 아니라 한 draw call이다.”

이 챕터에서 정복할 CG 개념

왜 이게 중요한가

CPU가 객체를 하나씩 그리라고 GPU에 명령하면 1000개일 때 1000번의 driver 왕복이 일어난다. 한 번 수 마이크로초의 비용이 모이면 vertex 처리 시간을 가볍게 추월한다. instancing은 "한 mesh, 다른 transform N번" 패턴을 단일 명령으로 압축한다. 14장의 raster pipeline 위에서 vertex shader가 N번 시작되되 instance attribute가 인스턴스마다 갱신된다. POP 측에서는 attribute 한 컬럼이 인스턴스의 위치·회전·스케일·색을 정한다. 결과는 같지만 비용 곡선이 다르다.

POP에서의 노출 지점

이론

per-vertex vs per-instance

vertex attribute는 보통 vertex마다 흐른다 (divisor = 0). 같은 buffer를 divisor = 1로 바인딩하면 instance 단위로 값이 바뀐다. instance 100, mesh당 vertex 24라면 per-vertex는 2400번, per-instance는 100번 fetch된다. draw call은 한 번, vertex shader는 instance × vertex 번 실행된다.

TouchDesigner의 instance attribute 데이터 경로

flowchart LR A["Box POP\n(mesh)"] --> G["Geometry COMP"] B["Grid POP\n(소스, P attribute)"] --> G G -- "instancing on,\ninstanceop = Grid POP,\ninstancetx/y/z = P" --> R["Render TOP"]

Geometry COMP는 두 입력을 받는다. 컴포넌트 안의 mesh (Box POP)와 Instance 페이지가 가리키는 외부 OP (Grid POP). 후자의 element 수가 인스턴스 수, 후자의 attribute가 per-instance 데이터다.

vertex shader 안에서

// 의사 코드 (TD가 내부에서 합성하는 것의 모양)
vec4 worldPos = TDInstanceMat(TDInstanceID()) * vec4(TDPos(), 1.0);
gl_Position = TDWorldToProj(worldPos);

TDInstanceMat()은 Instance 페이지의 translate/rotate/scale/pivot 파라미터를 모아 4×4 transform으로 만든 결과다. TDDeform(P)을 그대로 쓰면 skinning + instance + world 변환을 한 번에 처리한다.

직관과 어긋나는 부분

instancing은 draw call을 줄이지 vertex 작업을 줄이지 않는다. 인스턴스 100개라면 vertex shader는 여전히 vertex수 × 100번 실행된다 — 16장에서 그 함의를 다룬다. per-instance attribute가 vertex 메모리 대역폭을 줄여 주지도 않는다. 줄어드는 것은 CPU→GPU 호출 횟수이고 부수적으로 fetch 패턴이 일관되어 캐시 효율이 오른다.

손작업 (Hands-on)

시작 파일

연결: Box POP1을 Geometry COMP의 자식으로 둔다. Grid POP1은 root에 둔다.

단계 1 — instancing 활성화

Geometry COMP1의 Instance 페이지:

보게 될 것: 32×32=1024개의 박스가 grid 형태로 정렬된 모습. 모두 같은 mesh의 인스턴스다.

단계 2 — Phong MAT으로 단순 셰이딩

Geometry COMP의 Material에 Phong MAT을 붙인다. Phong MAT은 instancing-aware다 — TD가 자동 생성하는 셰이더가 내부에서 TDDeform을 거쳐 instance transform을 적용한다.

보게 될 것: 1024 회색 박스가 균일하게 음영을 받는다. Copy POP으로 1024번 복제한 mesh와 비교하면 cook 비용·메모리 사용이 크게 다르다. wiki 미확정 — Geometry COMP의 instancing path가 어느 시점에 GPU 명령으로 변환되는지의 내부 메커니즘은 wiki에 미노출. 실험 필요.

단계 3 — GLSL MAT으로 같은 결과를 손으로

GLSL MAT1을 만들어 Material을 교체한다.

vertex shader:

out Vertex { vec3 worldPos; vec3 worldNormal; int instID; } oVert;

void main() {
    vec4 worldPos = TDDeform(TDPos());     // instance + world 합성
    oVert.worldPos    = worldPos.xyz;
    oVert.worldNormal = TDDeformNorm(TDNormal());
    oVert.instID      = TDInstanceID();    // gl_InstanceID 직접 쓰지 않음
    gl_Position = TDWorldToProj(worldPos);
}

pixel shader:

in Vertex { vec3 worldPos; vec3 worldNormal; flat int instID; } iVert;
out vec4 fragColor;

void main() {
    vec3 N = normalize(iVert.worldNormal);
    vec3 L = normalize(vec3(0.3, 1.0, 0.5));
    float NdotL = max(dot(N, L), 0.0);
    // instance ID로 hue를 흔든다
    float h = fract(float(iVert.instID) * 0.0123);
    vec3 hue = 0.5 + 0.5*cos(6.2831*h + vec3(0.0, 2.0, 4.0));
    vec3 col = 0.1 + 0.9 * NdotL * hue;
    fragColor = TDOutputSwizzle(vec4(col, 1.0));
}

이 코드가 GPU에서 하는 일: vertex shader가 instance × vertex = 1024 × 8 = 8192번 호출된다. 매번 TDInstanceID()는 0..1023 중 하나를 반환하고 TDDeform이 그 인스턴스의 transform을 곱해 world position을 만든다. fragment shader는 instance ID를 flat으로 받아 (보간하지 말라는 의미) hue로 변환한다. draw call은 여전히 하나다.

보게 될 것: 1024 박스가 각자 다른 색조로 음영을 받는다. 모두 한 번의 draw call에서 나왔다.

단계 4 — POP의 custom attribute를 instance color로

Grid POP과 Geometry COMP 사이에 Random POP을 끼워 Cd (vec4) attribute를 만든다. Geometry COMP Instance 2 페이지: instancecolormode=replace, instancecolorop=색 POP 경로, instancer/g/b/a=Cd(0..3). pixel shader의 색을 instance color로 곱한다.

vec4 baseColor = vec4(col, 1.0);
baseColor = TDInstanceColor(baseColor);   // instance color 적용
fragColor = TDOutputSwizzle(baseColor);

보게 될 것: POP attribute가 직접 인스턴스 색을 결정한다. POP 측에서 색을 바꾸면 그래프 재컴파일 없이 갱신된다.

단계 5 — draw call 수 확인

Performance Monitor (Dialogs → Performance Monitor) 또는 GPU Info에서 Geometry COMP cook 시 draw call 카운트가 1 (또는 카메라당 1)에 머무는지 확인한다. wiki 미확정 — 정확한 라벨은 wiki에 명시 없음. 실험 필요. Copy POP으로 1024번 복제한 mesh와 비교하면 GPU 메모리 사용량이 mesh × 1024로 커진다.

노드 ↔︎ GLSL 매핑

같은 결과 — 1024 박스를 grid에 배치, 인스턴스마다 색.

노드: Box POP → Geometry COMP. Grid POP → Random POP(Cd) → Geometry COMP의 instanceop / instancecolorop. Material: Phong MAT.

GLSL MAT: 같은 그래프, Material을 GLSL MAT으로. vertex shader 한 줄 TDDeform(TDPos()), pixel shader 한 줄 TDInstanceColor(baseColor).

차이는 색의 출처가 노드 파라미터(instancecolormode=replace)냐 셰이더 코드냐다. 동작은 동일. Phong MAT은 위 GLSL MAT의 자동 생성판이다. 두 경로 모두 gl_InstanceID가 아니라 TDInstanceID()를 거쳐야 한다 (wiki: "should always be used, not gl_InstanceID directly") — TouchDesigner가 multi-camera 렌더에서 instance index를 재해석할 수 있기 때문이다.

확인 질문 (Self-check)

  1. 인스턴스 N개를 그릴 때 draw call은 몇 번 발생하는가? vertex shader 호출은 몇 번 발생하는가?
  2. Geometry COMP의 instanceop에 POP을 꽂으면 인스턴스 수는 어떻게 결정되는가?
  3. TDInstanceID()를 쓰는 이유는 무엇인가? gl_InstanceID를 직접 쓰면 어떤 상황에서 문제가 되는가?
  4. POP의 P attribute가 어떤 파라미터 경로를 통해 인스턴스의 translate로 매핑되는가? color는?
  5. instancing이 vertex 처리 비용을 줄이는가 아닌가? 그 답이 16장의 LOD 논의를 어떻게 견인하는가?

연결 고리

이 챕터의 한 줄 명제

Instancing은 한 mesh와 N개의 attribute 행을 단 한 번의 draw call로 묶는 약속이다. POP의 attribute는 그 행을 채우는 가장 자연스러운 데이터원이다.