이 챕터에서 정복할 CG 개념
- SSBO (Shader Storage Buffer Object)가 GPU 위 attribute의 실제 저장소라는 사실.
- 입력 attribute → 출력 attribute의 함수 시그니처 매핑.
TDIn_AttribName()패밀리. Initialize Output Attributes토글이 "기본 ON"이어야 하는 이유.- GLSL POP의 단일 attribute class 제약. 다른 class의 attribute는
TDInPoint_,TDInVert_,TDInPrim_접두사로만 읽는다. P[id] = TDIn_P()한 줄이 OpenGL의 VBO를 직접 mutate하는 가장 작은 GPU 프로그램이라는 사실.
왜 이게 중요한가
POP은 GPU 그래픽스 파이프라인의 데이터 모델을 노드 그래프로 노출시킨 학습 환경이다. 이 챕터에서 처음으로 그 데이터 모델을 노드가 아니라 코드로 직접 쓴다. 평소 fragment shader에서 gl_FragColor에 vec4를 대입하는 것은 framebuffer라는 SSBO 비슷한 것에 쓰는 일이고, 그 framebuffer는 다음 프레임의 sampler가 된다. GLSL POP에서 P[id] = ...에 vec3를 대입하는 것은 vertex buffer라는 SSBO에 쓰는 일이고, 그 buffer는 다음 노드의 입력, 혹은 Geometry COMP의 draw call 입력이 된다. 버퍼에 쓰는 가장 작은 프로그램을 손에 익히는 것이 본 챕터의 목표다. Ch.9 이후의 noise, particle, ping-pong, instancing, neighborhood — 모두 같은 Attr[id] = ... 한 줄을 변주한 것이다.
POP에서의 노출 지점
GLSL POP의 GLSL 페이지 파라미터 중 본 챕터에서 쓰는 것:
Compute Shader(computedat) — compute shader 코드가 들어 있는 Text DAT.Attribute Class(attrclass) —point/vertex/primitive중 하나. 본 노드 안의 모든 입출력은 이 class를 기준으로 동작한다.Number of Threads(numthreadsmode) —auto로 고정 (Ch.7).Output Attributes(outputattrs) — 이 셰이더가 쓸 attribute 이름. 와일드카드 가능.Output Access(outputaccess) —writeonly또는readwrite. atomic 연산이 필요하면readwrite.Initialize Output Attributes(initoutputattrs) — 기본 ON. 입력 attribute 값을 출력으로 한 번 복사하는 사전 dispatch를 실행한다.
Vectors 페이지에서 uniform 추가:
vec0name=uTime,vec0type=float등의 형태로 임의 uniform을 셰이더에 직접 노출 가능. 값에 expression(absTime.seconds같은)을 바인딩하면 매 cook마다 갱신된다.
GLSL 코드 측 핵심 시그니처 (wiki verbatim):
// 같은 class에서 읽기
attribType TDIn_AttribName(uint inputIndex, uint elementId, uint arrayIndex);
TDIn_AttribName() = TDIn_AttribName(0, TDIndex(), 0);
// 다른 class에서 읽기 (attribute class가 Point일 때 vertex/prim attribute 접근)
attribType TDInVert_AttribName(uint inputIndex, uint vertId, uint arrayIndex);
attribType TDInPrim_AttribName(uint inputIndex, uint primId, uint arrayIndex);
// 쓰기 — function이 아니라 array variable
attribType AttribName[];
이론
GLSL POP과 다른 GLSL POP의 차이
POP inventory에 따르면 GLSL 패밀리는 다섯 개다:
- GLSL POP: 한 attribute class에서만 동작. element 개수 불변. 본 챕터 주인공.
- GLSL Advanced POP: 모든 class에 동시에 쓰기. element 개수 변경 가능. Extra Outputs로 여러 POP에 동시 쓰기. Ch.12에서 본격.
- GLSL Copy POP: copy당 한 셰이더 호출. instance 변형의 GLSL 버전.
- GLSL Create POP: DEPRECATED. 새 작업에는 쓰지 않는다.
- GLSL Select POP: GLSL Advanced POP의 Extra Output 중 하나를 선택해 출력.
본 챕터는 가장 단순한 GLSL POP만 다룬다. 단일 class, 단일 출력, 단일 pass, 단일 dispatch — 즉 "한 attribute, 한 thread, 한 step"의 최소 단위. 이 단위에서 한 줄을 익히면 나머지 GLSL POP의 변주가 자연스럽게 따라온다.
Attribute = SSBO
TouchDesigner wiki의 단언: "Each attribute is a Shader Storage Buffer Object (SSBO)." 한 attribute(P, N, Cd, 사용자 정의 vel 등)는 GPU 메모리에 하나의 SSBO로 존재한다. point class의 P attribute이고 point가 1024개라면, P는 1024 × sizeof(vec3) = 12,288 byte의 buffer다. shader는 이 buffer를 vec3 P[]로 본다.
입력 P와 출력 P는 서로 다른 두 buffer다. TDIn_P()는 입력 buffer를 읽고, P[id] = ...는 출력 buffer에 쓴다. 이 분리 덕분에 모든 thread가 race 없이 동시 동작할 수 있다.
GLSL POP의 boilerplate — 작성자가 안 쓰는 부분이 있다
작성자가 Text DAT에 적는 코드는 void main() { ... } 한 함수다. 그러나 GPU가 실제로 실행하는 셰이더는 그것만이 아니다. GLSL POP은 컴파일 직전에 자동으로 boilerplate을 앞뒤로 붙인다.
boilerplate에 들어가는 것들:
#version 460같은 버전 declaration.layout(local_size_x = ..., local_size_y = ..., local_size_z = ...) in;— workgroup size.- 모든 입력 attribute에 대한 SSBO declaration (readonly).
- 모든 출력 attribute에 대한 SSBO declaration (writeonly 또는 readwrite).
- Vectors / Samplers / Arrays / Matrices 등 파라미터 페이지에서 정의한 uniform.
TDIndex(),TDNumElements(),TDIn_AttribName()등 helper 함수.- (optional)
TDSimplexNoise()함수 정의.
작성자는 위 모든 것을 명시적으로 declare하지 않아도 된다. 셰이더의 헤더 영역은 GLSL POP의 파라미터 페이지가 책임진다. 이 추상화 덕분에 작성자는 SSBO binding 인덱스, layout qualifier, version directive 같은 boilerplate를 신경 쓰지 않고 알고리즘에만 집중할 수 있다.
trade-off: 어떤 보일러플레이트가 어떻게 생성되는지를 직접 볼 수 있는 toggle이 wiki에 명시되지 않는다 — wiki 미확정 — 실험 필요. textport에 컴파일 에러가 나면 그때 자동 생성된 코드의 일부가 라인 번호로 노출되기도 한다.
Output Attributes 파라미터의 의미
Output Attributes에 P를 적어 두지 않으면 셰이더 안에서 P[id] = ...는 컴파일되지 않는다. wiki 발췌: "For P[id] = TDIn_P(); to compile without error, P needs to be present in the input, and selected for writing in 'Output Attributes'."
Output Attributes는 와일드카드(*)도 받는다. 모든 attribute를 출력으로 노출하려면 *. 특정 attribute만 출력하려면 이름 나열.
수정하지 않은 attribute는 downstream POP으로 자동으로 reference 전달된다 (메모리 절약). wiki: "Unmodified input attributes for the POP from the first input are passed to downstream POP with references and don't use extra memory."
입력이 여러 개일 때 — 두 sphere를 한 셰이더에서 다루기
GLSL POP은 여러 입력을 받는다. 첫째 입력에서 P를 읽고 둘째 입력에서 다른 attribute를 가져와 합칠 수 있다.
void main() {
const uint id = TDIndex();
if (id >= TDNumElements()) return;
vec3 p0 = TDIn_P(0, id); // 첫째 입력의 P
vec3 p1 = TDIn_P(1, id); // 둘째 입력의 P
P[id] = mix(p0, p1, 0.5); // 두 mesh를 blend
}
조건: 두 입력의 attribute class와 element 개수가 같아야 한다. 점 개수가 다르면 TDIn_P(1, id)의 id가 bound를 벗어날 수 있다 — 그러면 wiki가 보장하는 대로 마지막 element가 반환된다 (크래시는 아님).
이런 패턴은 morph target, blend shape, vertex animation의 기초다. point가 동일한 두 sphere를 가져와 morph 비율로 섞으면 vertex animation의 가장 단순한 형태가 된다.
Initialize Output Attributes — 왜 ON이 안전한가
이 토글이 ON이면 사용자의 compute shader가 호출되기 전에, GLSL POP은 별도의 사전 dispatch를 한 번 더 돌려 입력 attribute의 값을 출력 buffer에 그대로 복사한다.
OFF로 두면 두 가지 결과 중 하나다:
- 셰이더 코드가 모든 element를 빠짐없이 쓴다 → 정상.
- 셰이더 코드가 일부 element만 쓴다 → 나머지 element는 초기화되지 않은 GPU 메모리의 쓰레기 값을 가진다.
wiki 경고 verbatim: "Reading from unitialized values in downstream POPs can cause unpredictable behavior and crashes."
원칙: 모든 element를 매 frame 빠짐없이 쓰는 것이 확실한 셰이더에 한해서만 OFF로 끄고 한 번의 dispatch 비용을 절약한다. 의심스러우면 ON.
Output Access — writeonly vs readwrite
writeonly는 SSBO에 writeonly 한정자를 붙인다. 컴파일러는 같은 thread에서 같은 element를 read-after-write 못 하게 강제하고, 그 대신 더 공격적으로 최적화한다.
readwrite는 atomic 연산을 위해 필요하다. atomic은 같은 element를 여러 thread가 동시에 갱신할 때 race 없이 누적하려는 패턴 (histogram, particle compaction 등)이다. wiki 발췌: "Read-Write is necessary to perform atomic operations. In that case the output values should be initialized first, either using the toggle parameter, or during the first pass in a multi-pass POP (GLSL POP only)."
본 챕터는 한 thread가 한 element만 다루는 가장 단순한 1-pass 셰이더이므로 writeonly로 충분하다.
Attribute 종류별 layout과 component 수
GLSL POP에서 attribute는 type과 component 수로 정의된다. Create Attributes 페이지의 attr0type은 float / int 등 base type. attr0numcomps는 component 수 (1=scalar, 2=vec2, 3=vec3, 4=vec4).
자주 쓰이는 attribute의 표준 layout:
| Attribute | type | comps | 용도 |
|---|---|---|---|
| P | float | 3 | 위치 vec3 |
| N | float | 3 | normal vec3 |
| Cd | float | 3 | RGB color (혹은 4 for RGBA) |
| Tex | float | 3 | texture coord (보통 .xy 사용, z는 layer) |
| vel | float | 3 | velocity vec3 |
| age | float | 1 | scalar |
| pscale | float | 1 | scalar |
| Alpha | float | 1 | scalar (혹은 Cd의 .a로 포함) |
| pid | int | 1 | particle id |
attr0isarray를 켜면 한 element가 array를 가진다. 예: bone weight 4개 = float, comps=1, isarray=on, arraysize=4. matrix attribute는 별도 matattr 블록.
다른 class의 attribute는 어떻게 읽나
GLSL POP은 한 노드에서 하나의 attribute class에만 쓴다. 그러나 읽기는 다른 class에서도 가능하다.
본 노드의 attrclass |
같은 class 읽기 | 다른 class 읽기 |
|---|---|---|
point |
TDIn_AttribName() |
TDInVert_AttribName(), TDInPrim_AttribName() |
vertex |
TDIn_AttribName() |
TDInPoint_AttribName(), TDInPrim_AttribName() |
primitive |
TDIn_AttribName() |
TDInPoint_AttribName(), TDInVert_AttribName() |
point에 쓰면서 vertex의 어떤 정보를 읽고 싶으면 TDInVert_AttribName()을 쓰면 된다. 단, 쓰기는 본 노드의 class에만 가능하다. 세 class에 동시에 쓰는 셰이더가 필요하면 GLSL Advanced POP을 써야 한다 (Ch.12).
Time uniform — 자동? 수동?
fragment shader에서 흔히 쓰는 iTime 같은 시간 입력이 GLSL POP에서 자동 노출되는지는 wiki 미확정 — 실험 필요. wiki는 GLSL POP의 자동 uniform 목록을 명시하지 않는다. 안전한 패턴은 Vectors 페이지에 직접 float 하나를 만들고 expression으로 absTime.seconds를 바인딩하는 것이다.
std140 / std430 layout과 alignment
POP attribute가 SSBO로 저장된다는 사실에는 한 가지 함의가 따라온다 — 메모리 정렬 규칙이다. GLSL의 SSBO는 보통 std430 layout을 따른다 (std140보다 packing이 더 친밀하다). 작성자는 한 attribute의 component 수를 정할 때 이 규칙을 의식해야 한다.
- vec3는 16 byte 경계에 정렬된다 — vec3 attribute는 메모리에서 vec4처럼 자리를 차지할 수 있다.
- vec4는 자연스럽게 16 byte.
- float / int는 4 byte.
- array of struct에서 struct 안에 vec3가 있으면 padding이 들어간다.
POP의 attribute는 보통 하나의 component 종류만(예: vec3 P) 가지므로 작성자가 직접 layout을 짜는 일은 거의 없다. 그러나 TDIn_P()가 vec3를 반환하고 그 vec3가 GPU 메모리에서 어떻게 자리잡는지를 알면 왜 vec3 attribute의 메모리가 vec4 attribute와 거의 같은지 같은 의문이 풀린다. LearnOpenGL의 "Advanced GLSL" 페이지 std140 절이 이 규칙의 표준 참고처다.
Uniform이 셰이더에 등장하는 방식
Vectors 페이지에 uTime이라는 float을 만들면, 셰이더 안에서 이 이름이 자동으로 노출된다. 작성자가 직접 uniform float uTime;을 적는 것은 명시적이지만, wiki에 따르면 boilerplate가 자동 declare하는 패턴이다.
실용 관찰:
- 작성자가 명시적 declaration을 추가해도 충돌이 일어나지 않는다 (같은 type/name이면 OK, 다르면 컴파일 에러).
- 명시적 declaration이 있으면 셰이더 텍스트만 읽고도 어떤 uniform을 쓰는지가 분명해진다. 권장 패턴이다.
Samplers 페이지에 추가한 TOP은 sampler2D 같은 형태로 노출되고, texture(samplerName, uv) 호출로 sampling. Arrays 페이지의 CHOP은 array 또는 texture buffer로 노출 (array0arraytype 선택). Matrices 페이지의 4x4 행렬은 mat4로 자동 declare.
Specialization constants (Constants 페이지)는 셰이더 컴파일 시점에 baked in 되는 상수다. 일반 uniform과 달리 매 dispatch마다 갱신되지 않으므로 inline 최적화가 가능하다. 본 챕터에서는 다루지 않지만 향후 성능 튜닝에서 유용.
입력 attribute가 reference로 전달된다는 사실
wiki 발췌: "Unmodified input attributes for the POP from the first input are passed to downstream POP with references and don't use extra memory."
본 GLSL POP이 P만 수정하고 N, Cd 같은 다른 attribute를 건드리지 않으면, downstream POP의 입력에는 N과 Cd의 새 buffer가 만들어지지 않고 입력 POP의 buffer가 그대로 참조된다. info panel에는 (r) 표시가 붙는다.
이 동작은 GPU 메모리 절약 측면에서 중요하다. attribute가 50개라도 GLSL POP에서 P만 수정하면 49개의 attribute는 새 메모리를 할당하지 않는다. 단, Output Attributes에 *로 모든 attribute를 등록하면 모두 allocation된다. 의도하지 않은 메모리 낭비를 막으려면 정말 수정하는 attribute만 명시한다.
TouchDesigner의 GLSL 버전과 그 의미
wiki 발췌: "TouchDesigner's main supported version of GLSL is 4.60." compute shader는 GLSL 4.30 이상에서 가능하다.
GLSL 4.60은 다음을 포함한다:
- compute shader (4.30+)
- atomic 연산 (4.30+)
- shader storage buffer object (4.30+)
- subgroup operations (4.60+, 일부 확장)
- explicit uniform location (4.30+)
이 사실이 의미하는 것: GLSL POP 안에서 modern GLSL의 거의 모든 도구가 쓸 수 있다. imageStore, atomicAdd, barrier(), memoryBarrierShared() 등은 모두 가용. 단, GLSL POP은 shared memory 패턴을 wiki에서 별도로 문서화하지 않는다 — manual workgroup size에서는 가능할 것으로 추정되지만 wiki 미확정 — 실험 필요.
같은 셰이더의 두 dispatch가 결과가 다를 수 있는 경우
GLSL POP은 한 번의 dispatch만 보장한다. 같은 셰이더, 같은 입력, 같은 파라미터에서는 매번 같은 결과가 나온다 — 그것이 deterministic의 정의다. 그러나 다음 경우에는 결과가 다를 수 있다:
- 입력에 시간 의존 uniform이 있을 때 (
absTime.seconds). 두 dispatch가 다른 시간에 일어나면 결과 다름. 의도된 동작. - 입력 POP의 데이터가 frame 사이에 변할 때 (예: 입력이 Noise POP인데 Noise POP이 시간 의존). 같음.
- random seed가 없는 셰이더 안에서 hash나 GLSL
noise()를 호출할 때 — TouchDesigner의TDSimplexNoise()는 deterministic하지만 GLSL built-innoise()는 구현 의존이라 추천하지 않음. Output Access = readwrite이고 atomic operation을 thread 간에 사용할 때 — 누적 순서가 보장되지 않아 floating point reduction 결과가 미세하게 다를 수 있음.
본 챕터의 단순 셰이더는 위 어디에도 해당하지 않으므로 완전한 deterministic이다.
입력 SSBO와 출력 SSBO의 분리 — race condition이 없는 이유
POP의 GLSL 모델에서 입력 P와 출력 P는 같은 이름을 가지지만 서로 다른 두 SSBO다. TDIn_P(0, j)는 입력 buffer의 j번 element를 읽고, P[id] = ...는 출력 buffer의 id번 element에 쓴다. 두 buffer가 분리되어 있기 때문에:
- thread A가 자기 id의 출력을 쓰는 동안, thread B가 같은 id의 입력을 읽는 시나리오에서도 race condition이 없다.
- 한 thread가 자기 id 외의 다른 j번 입력 P를 읽는 neighborhood operation도 안전하다 (입력은 dispatch 동안 불변).
이 분리는 GPU 병렬화의 핵심 enabler다. CPU에서 같은 배열을 in-place로 수정하면 element 순서 의존성이 생기지만, GPU에서는 dispatch 전체가 한 atomic step처럼 동작한다. functional programming의 immutability와 같은 효과를 GPU 하드웨어로 강제한 것.
단, Output Access = readwrite이고 atomic 연산을 쓰면 출력 buffer가 mutate 가능해진다. 이때는 race condition을 신경 써야 한다.
TDInCache_ 패밀리 — Cache POP과의 짝
GLSL POP은 단일 frame의 입력만 보지 않는다. wiki verbatim signature:
attribType TDInCache_AttribName(uint inputIndex, uint cacheIndex, uint elemId, uint arrayIndex);
이 함수는 Cache POP의 N-frame history 중 cacheIndex번째 frame에서 attribute를 읽는다. trail / motion blur / temporal smoothing 같은 작업이 한 GLSL POP 안에서 가능해진다. point가 1024개고 cache가 60 frame이라면 GPU에는 1024 × 60 = 61,440개의 vec3가 보존되어 있고, 셰이더는 그 어느 element든 직접 읽을 수 있다.
Boundary handling — 함수가 자동으로 막아주는 것
wiki 발췌: "this performs bounds checking, return last elements if outside of bounds, last arrayIndex if arrayIndex is out of bounds."
TDIn_AttribName()은 elementId가 범위를 벗어나면 마지막 element를 반환한다. 즉 셰이더 안에서 elementId 계산이 잘못되어도 크래시는 나지 않는다. 단, 그 데이터가 의도와 맞는지는 작성자가 검증해야 한다. 이 보호막은 디버깅 비용을 크게 줄인다.
대비: P[id] = ...에서 id가 범위를 벗어나면 메모리 손상이 일어난다. 입력에는 안전망이 있고, 출력에는 없다. 그래서 출력 write 직전에 항상 bounds-check 한다.
셰이더 컴파일은 언제 일어나는가
GLSL POP의 Compute Shader DAT 내용이 변경되면, POP이 다음 cook 시 셰이더를 재컴파일한다. 컴파일은 driver가 GLSL 텍스트를 SPIR-V 중간 표현 후 GPU 네이티브 머신 코드로 번역하는 과정이다. 처음 cook은 수 ms ~ 수십 ms 걸릴 수 있고, 이후 cook은 0에 가까운 비용.
파라미터 페이지의 uniform 정의가 바뀌면 (Vectors에 새 항목 추가, type 변경 등) 셰이더 boilerplate가 바뀌므로 재컴파일이 일어난다. 입력 POP의 attribute 목록이 바뀌어도 마찬가지.
작성 중 자주 셰이더 텍스트를 수정하면 매번 컴파일이 일어난다. 이것이 일반적인 작업 흐름이고, 컴파일 비용은 작업을 방해할 만큼 크지는 않다.
textport에서 컴파일 에러 메시지를 본다. 에러 메시지의 줄 번호는 boilerplate가 앞에 붙은 결합된 셰이더의 줄 번호이므로, 사용자가 작성한 코드의 줄 번호와 어긋날 수 있다.
출력에만 등록한 새 attribute
Output Attributes에 입력에 없는 이름을 넣으면 어떻게 되나? 그것은 새 attribute이고, Create Attributes 페이지에서 type을 정의해야 한다.
Create Attributes:
attr0name = density
attr0type = float
attr0numcomps = 1
attr0value0 = 0.0 (default value)
이렇게 등록한 density는 셰이더 안에서:
TDIn_density()— 입력에는 없지만 default 0.0을 반환하는지, 컴파일 에러인지는 wiki 미확정 — 실험 필요.density[id] = ...— 출력에 쓰기는 자유.
새 attribute는 downstream POP에서 일반 attribute처럼 보인다. GPU 측에서는 새 SSBO 한 개가 할당된다. point 개수 × component 개수 × 4 byte. density 같은 float attribute는 1,000,000 point에서 4 MB.
성능 측면 — GLSL POP 한 번의 비용
본 챕터의 단순 셰이더 한 번의 비용을 가늠해 보자. 입자가 100,000개, NVIDIA RTX 3070 기준.
- thread 띄우기: 100,000 thread, workgroup size 32 → ~3,125 workgroup.
- per-thread: SSBO read 1번 (vec3, 12 byte), sin 한 번, SSBO write 1번 (vec3, 12 byte).
- per-thread ALU: 약 10 명령.
- 총 ALU: 100,000 × 10 = 1M 명령.
- 총 memory traffic: 100,000 × 24 byte = 2.4 MB.
RTX 3070의 ALU throughput은 ~20 TFLOPS, memory bandwidth는 ~450 GB/s.
- ALU 시간: 1M / 20T = 0.00005 ms.
- Memory 시간: 2.4 MB / 450 GB/s ≈ 0.005 ms.
이론적 최소는 0.005 ms, 즉 5 µs. 실제로는 dispatch overhead (드라이버, 명령 buffer 생성), SSBO binding, 등 추가 비용으로 ~0.1–1 ms가 일반적이다. 100,000 입자를 60 fps로 갱신해도 GPU 시간의 1/16 ms = 6%만 소비.
이 분석이 의미하는 것: 단일 GLSL POP의 비용은 보통 무시할 수 있다. 병목은 같은 frame에 도는 다른 작업(render, multi-pass shading)이 거의 항상 더 크다.
손작업 (Hands-on)
시작 파일
- Sphere POP1 (Primitive Type: Geodesic, Frequency: 4)
- Text DAT1 (compute shader 코드)
- GLSL POP1
- Geometry COMP1
- Render TOP1
연결: Sphere POP1 → GLSL POP1 → Geometry COMP1 → Render TOP1
GLSL POP1 파라미터:
Compute Shader:Text DAT1Attribute Class:pointNumber of Threads:autoOutput Attributes:POutput Access:writeonlyInitialize Output Attributes: On- Vectors 페이지:
vec0name=uTime,vec0type=float,vec0value= expressionabsTime.seconds
Step 1 — sine wave displacement
Text DAT1:
// Sphere를 y축으로 출렁이게 하는 가장 작은 GPU 프로그램.
// Vectors 페이지에서 uTime이라는 float uniform을 노출시켜야 한다.
uniform float uTime;
void main() {
const uint id = TDIndex();
if (id >= TDNumElements()) return;
vec3 p = TDIn_P();
p.y += sin(p.x * 4.0 + uTime) * 0.2;
P[id] = p;
}
이 코드가 GPU에서 실제로 하는 일: GPU는 sphere의 point 개수만큼 thread를 띄운다 (NVIDIA는 32 thread 단위로 올림). 각 thread는 자기 인덱스 TDIndex()의 P attribute를 입력 SSBO에서 vec3로 읽는다. 그 vec3의 y 성분에 시간과 x좌표에 따른 sin 값을 더한다. 결과 vec3를 같은 인덱스의 출력 P SSBO에 쓴다. 이 모든 일이 1024 (혹은 그 이상)개의 thread에서 동시에 일어난다. element 간 의존성이 없으므로 어떤 순서로 끝나든 결과가 같다.
Step 2 — TDTime 자동 노출 옵션
위 코드에서 uniform float uTime; 선언을 지우고, 셰이더 본문에서 uTime 자리에 TDTime.absTime 같은 표현을 시도해 본다.
void main() {
const uint id = TDIndex();
if (id >= TDNumElements()) return;
vec3 p = TDIn_P();
// TDTime이 자동으로 노출되는지: wiki 미확정 — 실험 필요
// p.y += sin(p.x * 4.0 + TDTime.absTime) * 0.2;
P[id] = p;
}
본 핸드북에서는 Vectors 페이지로 직접 바인딩하는 Step 1의 방식이 권장 패턴이다. TDTime이 노출되더라도 명시적 uniform 바인딩이 의도와 의존성을 더 분명히 한다.
Step 3 — Initialize Output Attributes 끄기 실험
위 셰이더는 모든 thread가 매 frame P[id]에 무조건 쓴다. 따라서 Initialize Output Attributes를 OFF로 꺼도 정상 동작한다. OFF로 바꾸고 결과가 같은지 확인.
이제 셰이더를 다음과 같이 바꿔 일부 thread만 쓰게 만든다.
uniform float uTime;
void main() {
const uint id = TDIndex();
if (id >= TDNumElements()) return;
vec3 p = TDIn_P();
if (mod(float(id), 2.0) < 0.5) { // 짝수 id만 쓴다
p.y += sin(p.x * 4.0 + uTime) * 0.2;
P[id] = p;
}
// 홀수 id는 P[id]에 안 쓴다 → 미초기화 메모리
}
Initialize Output Attributes OFF → 홀수 id의 P가 GPU 메모리 쓰레기 값. 화면이 깨진다.
Initialize Output Attributes ON → 홀수 id의 P는 입력값으로 자동 초기화. 짝수 id만 출렁인다.
관찰: ON이 왜 기본이고 왜 안전한지가 이 실험에서 가시화된다.
Step 4 — Output Attributes 비워두기
Output Attributes를 빈 칸으로 두면 P[id] = ...가 컴파일 에러를 낸다. 셰이더의 declare는 GLSL POP이 파라미터에서 자동 생성하기 때문이다.
노드 ↔︎ GLSL 매핑
같은 sine displacement를 노드만으로 구성한다.
노드 버전
Sphere POP1
→ Noise POP1 ?? ← Noise POP은 sin이 아니라 noise field.
순수 sin 변형은 Math POP의 Function 메뉴가 더 적합.
→ Math POP1 (Attribute: P, Function: ?)
→ Geometry COMP1
→ Render TOP1
순수 p.y += sin(p.x*4 + t)*0.2를 노드로 정확히 재현하려면 한 단계가 어렵다. POP inventory에서 Math POP은 "Takes one input attribute, performs math, writes result to a chosen attribute"이고, Math Combine POP은 "70+ operations"를 제공한다. 본 챕터 범위에서 노드 버전과 GLSL 버전이 완전히 동등한 결과를 내는 것보다 중요한 관찰은 다음이다:
- 노드 버전: 의도를 표현하려면 보통 2~4개의 POP을 chain해야 한다 (예: Math Combine POP으로
sin(p.x*4 + t)를 새 attribute로 만들고, 다시 Math POP으로 그 값을 P.y에 더하기). - GLSL 버전: 같은 의도를 한 줄로 표현한다.
| 측면 | 노드 chain | GLSL POP |
|---|---|---|
| 코드 줄 수 | 0 (그래프만) | 3~5 줄 |
| 표현력 | 노드가 지원하는 함수에 한정 | GLSL 전체 함수 (smoothstep, mix, mat3, ...) |
| 임시 attribute | 중간 단계마다 attribute 필요 | 로컬 변수 (vec3 p) |
| 메모리 | 각 노드 출력이 새 SSBO | 한 dispatch 안에서 끝 |
| 디버깅 | 각 단계를 viewer로 직접 봄 | printf 없음. shader 컴파일 에러 의존 |
여기서 중요한 것: 노드 기반은 데이터 흐름의 각 단계가 SSBO로 물질화된다는 점을 가르치고, GLSL POP은 한 dispatch 안에서 임의의 산술이 가능하다는 점을 가르친다. 둘은 같은 GPU 모델의 두 면이다.
GLSL POP 작성의 표준 절차
본 챕터의 단순 displacement에서 출발해, 새 GLSL POP을 만들 때 거치는 순서를 정리한다.
어느 attribute class에 쓸지 정한다. point displacement면 point. mesh normal recalc이면 vertex. primitive 단위 작업(예: triangle 면적 계산)이면 primitive. 한 노드에서 한 class.
입력 POP의 attribute 목록을 확인한다. Info CHOP 또는 viewer로. 셰이더에서 읽으려는 attribute가 입력에 존재해야 한다. 없으면 Create Attributes에서 default와 함께 만든다.
출력 attribute를 결정하고
Output Attributes파라미터에 적는다. 와일드카드*는 모든 attribute를 출력으로 노출 (메모리 비용 ↑). 명시적 나열이 권장.Number of Threads를auto로 둔다. 특별한 이유가 없는 한 manual은 피한다.Compute Shader DAT을 만들고 bounds-check 관용구로 시작한다:
void main() { const uint id = TDIndex(); if (id >= TDNumElements()) return; // ... AttribName[id] = result; }Initialize Output Attributes는 ON으로 둔다. OFF로 끄는 결정은 모든 element를 매 frame 빠짐없이 쓴다는 확신이 있을 때만.uniform이 필요하면 Vectors / Colors / Samplers / Arrays / Matrices 페이지에 추가한다. 이름은 셰이더에서 그대로
uniform <type> <name>;으로 자동 노출된다.컴파일 에러를 확인한다. GLSL POP의 info DAT 또는 textport. 에러 메시지는 셰이더 줄 번호 기준이지만, 자동 생성된 boilerplate(SSBO declaration 등)가 앞에 붙으므로 줄 번호가 어긋날 수 있다.
결과를 viewer에서 확인한다. POP viewer / Geometry COMP + Render TOP 양쪽. 예상과 다르면 셰이더 안에서 출력을 단순화해 isolate.
점차 복잡한 식을 추가한다. 한 번에 한 변화. 작동하는 코드에서 한 줄씩 추가해 문제 발생 지점을 분리.
디버깅 패턴
shader에는 printf가 없다. 디버깅은 시각화로 한다.
패턴 1 — id를 색으로 시각화
Output Attributes에 Cd를 추가하고:
Cd[id] = vec3(float(id) / float(TDNumElements()), 0.0, 1.0 - float(id) / float(TDNumElements()));
id 분포를 그라데이션으로 본다. 첫 id와 마지막 id 사이가 매끄러우면 dispatch가 정상이다.
패턴 2 — 중간값을 색으로 export
displacement가 이상하게 보일 때, displacement 양 자체를 색으로:
float n = TDSimplexNoise(p * 1.5);
Cd[id] = vec3(n * 0.5 + 0.5); // -1..1 → 0..1
P[id] = p + normalize(p) * n * 0.3;
displacement 크기가 noise 값을 시각적으로 검증.
패턴 3 — 한 attribute로 isolate
문제가 P인지 N인지 확실치 않으면 한 attribute만 출력에 두고 다른 것은 노드 chain 밖으로 빼낸다. 변수를 줄여 원인을 좁힘.
패턴 4 — Output Access를 readwrite로
같은 thread가 자기가 쓴 값을 다시 읽으려면 readwrite가 필요하다. 디버깅 중 임시로 readwrite로 두고 중간값을 재확인할 수 있다.
확인 질문 (Self-check)
Output Attributes파라미터에 attribute 이름을 적지 않으면P[id] = ...가 컴파일되지 않는다. 왜인가? GLSL POP은 셰이더 텍스트에서 어떤 정보를 자동으로 추출해 사용하나?Initialize Output Attributes를 OFF로 두는 것이 안전한 정확한 조건은? 한 줄로 답하라.Output Access를readwrite로 바꿔야 하는 시나리오 하나를 구체적으로 들어라 (atomic이 왜 필요한가).- attribute class를
point로 둔 GLSL POP 안에서 vertex의 어떤 값을 읽고 싶을 때, 어떤 함수 이름을 호출하는가? - 입력 P와 출력 P가 서로 다른 SSBO이기 때문에 가능한 일이 무엇인가? 만약 같은 buffer라면 어떤 race condition이 생기나?
연결 고리
- TouchDesigner Wiki — GLSL POP: https://docs.derivative.ca/GLSL_POP. 본 챕터의 1차 출처.
- TouchDesigner Wiki — Write a GLSL POP: https://docs.derivative.ca/Write_a_GLSL_POP. 함수 시그니처와 minimal example의 verbatim 출처.
- LearnOpenGL — Shaders: https://learnopengl.com/Getting-started/Shaders. "Ins and outs", "Uniforms" 절. GLSL POP의 uniform 노출 패턴이 OpenGL 일반의 그것과 같다.
- LearnOpenGL — Advanced GLSL: https://learnopengl.com/Advanced-OpenGL/Advanced-GLSL. "Interface blocks", "Uniform buffer objects (std140 layout)" 절. POP 내부 buffer layout의 배경.
- Khronos OpenGL Wiki — Buffer Object: https://wikis.khronos.org/opengl/Buffer_Object. SSBO를 비롯한 GL buffer object의 normative reference.
이 챕터의 한 줄 명제
P[id] = TDIn_P(); 한 줄은 vertex buffer라는 SSBO에 직접 쓰는 가장 작은 GPU 프로그램이다. 이 한 줄을 변주한 것이 Ch.9 이후의 모든 셰이더다.